namespace ts.server { export enum ProjectKind { Inferred, Configured, External } /* @internal */ export type Mutable = { -readonly [K in keyof T]: T[K]; }; /* @internal */ export function countEachFileTypes(infos: ScriptInfo[], includeSizes = false): FileStats { const result: Mutable = { js: 0, jsSize: 0, jsx: 0, jsxSize: 0, ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, deferred: 0, deferredSize: 0, }; for (const info of infos) { const fileSize = includeSizes ? info.getTelemetryFileSize() : 0; switch (info.scriptKind) { case ScriptKind.JS: result.js += 1; result.jsSize! += fileSize; break; case ScriptKind.JSX: result.jsx += 1; result.jsxSize! += fileSize; break; case ScriptKind.TS: if (fileExtensionIs(info.fileName, Extension.Dts)) { result.dts += 1; result.dtsSize! += fileSize; } else { result.ts += 1; result.tsSize! += fileSize; } break; case ScriptKind.TSX: result.tsx += 1; result.tsxSize! += fileSize; break; case ScriptKind.Deferred: result.deferred += 1; result.deferredSize! += fileSize; break; } } return result; } function hasOneOrMoreJsAndNoTsFiles(project: Project) { const counts = countEachFileTypes(project.getScriptInfos()); return counts.js > 0 && counts.ts === 0 && counts.tsx === 0; } export function allRootFilesAreJsOrDts(project: Project): boolean { const counts = countEachFileTypes(project.getRootScriptInfos()); return counts.ts === 0 && counts.tsx === 0; } export function allFilesAreJsOrDts(project: Project): boolean { const counts = countEachFileTypes(project.getScriptInfos()); return counts.ts === 0 && counts.tsx === 0; } /* @internal */ export function hasNoTypeScriptSource(fileNames: string[]): boolean { return !fileNames.some(fileName => (fileExtensionIs(fileName, Extension.Ts) && !fileExtensionIs(fileName, Extension.Dts)) || fileExtensionIs(fileName, Extension.Tsx)); } /* @internal */ export interface ProjectFilesWithTSDiagnostics extends protocol.ProjectFiles { projectErrors: readonly Diagnostic[]; } export interface PluginCreateInfo { project: Project; languageService: LanguageService; languageServiceHost: LanguageServiceHost; serverHost: ServerHost; config: any; } export interface PluginModule { create(createInfo: PluginCreateInfo): LanguageService; getExternalFiles?(proj: Project): string[]; onConfigurationChanged?(config: any): void; } export interface PluginModuleWithName { name: string; module: PluginModule; } export type PluginModuleFactory = (mod: { typescript: typeof ts }) => PluginModule; /** * The project root can be script info - if root is present, * or it could be just normalized path if root wasnt present on the host(only for non inferred project) */ /* @internal */ export interface ProjectRootFile { fileName: NormalizedPath; info?: ScriptInfo; } interface GeneratedFileWatcher { generatedFilePath: Path; watcher: FileWatcher; } type GeneratedFileWatcherMap = GeneratedFileWatcher | Map; function isGeneratedFileWatcher(watch: GeneratedFileWatcherMap): watch is GeneratedFileWatcher { return (watch as GeneratedFileWatcher).generatedFilePath !== undefined; } export abstract class Project implements LanguageServiceHost, ModuleResolutionHost { private rootFiles: ScriptInfo[] = []; private rootFilesMap = createMap(); private program: Program | undefined; private externalFiles: SortedReadonlyArray | undefined; private missingFilesMap: Map | undefined; private generatedFilesMap: GeneratedFileWatcherMap | undefined; private plugins: PluginModuleWithName[] = []; /*@internal*/ private packageJsonFilesMap: Map | undefined; /*@internal*/ /** * This is map from files to unresolved imports in it * Maop does not contain entries for files that do not have unresolved imports * This helps in containing the set of files to invalidate */ cachedUnresolvedImportsPerFile = createMap(); /*@internal*/ lastCachedUnresolvedImportsList: SortedReadonlyArray | undefined; /*@internal*/ private hasAddedorRemovedFiles = false; private lastFileExceededProgramSize: string | undefined; // wrapper over the real language service that will suppress all semantic operations protected languageService: LanguageService; public languageServiceEnabled: boolean; readonly trace?: (s: string) => void; readonly realpath?: (path: string) => string; /*@internal*/ hasInvalidatedResolution: HasInvalidatedResolution | undefined; /*@internal*/ resolutionCache: ResolutionCache; private builderState: BuilderState | undefined; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ private updatedFileNames: Map | undefined; /** * Set of files that was returned from the last call to getChangesSinceVersion. */ private lastReportedFileNames: Map | undefined; /** * Last version that was reported. */ private lastReportedVersion = 0; /** * Current project's program version. (incremented everytime new program is created that is not complete reuse from the old one) * This property is changed in 'updateGraph' based on the set of files in program */ private projectProgramVersion = 0; /** * Current version of the project state. It is changed when: * - new root file was added/removed * - edit happen in some file that is currently included in the project. * This property is different from projectStructureVersion since in most cases edits don't affect set of files in the project */ private projectStateVersion = 0; protected isInitialLoadPending: () => boolean = returnFalse; /*@internal*/ dirty = false; /*@internal*/ hasChangedAutomaticTypeDirectiveNames = false; /*@internal*/ typingFiles: SortedReadonlyArray = emptyArray; /*@internal*/ originalConfiguredProjects: Map | undefined; /*@internal*/ getResolvedProjectReferenceToRedirect(_fileName: string): ResolvedProjectReference | undefined { return undefined; } private readonly cancellationToken: ThrottledCancellationToken; public isNonTsProject() { updateProjectIfDirty(this); return allFilesAreJsOrDts(this); } public isJsOnlyProject() { updateProjectIfDirty(this); return hasOneOrMoreJsAndNoTsFiles(this); } public static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void, logErrors?: (message: string) => void): {} | undefined { const resolvedPath = normalizeSlashes(host.resolvePath(combinePaths(initialDir, "node_modules"))); log(`Loading ${moduleName} from ${initialDir} (resolved to ${resolvedPath})`); const result = host.require!(resolvedPath, moduleName); // TODO: GH#18217 if (result.error) { const err = result.error.stack || result.error.message || JSON.stringify(result.error); (logErrors || log)(`Failed to load module '${moduleName}' from ${resolvedPath}: ${err}`); return undefined; } return result.module; } /*@internal*/ readonly currentDirectory: string; /*@internal*/ public directoryStructureHost: DirectoryStructureHost; /*@internal*/ public readonly getCanonicalFileName: GetCanonicalFileName; /*@internal*/ readonly packageJsonCache: PackageJsonCache; /*@internal*/ private importSuggestionsCache = Completions.createImportSuggestionsForFileCache(); /*@internal*/ private dirtyFilesForSuggestions: Map | undefined; /*@internal*/ private symlinks: ReadonlyMap | undefined; /*@internal*/ constructor( /*@internal*/ readonly projectName: string, readonly projectKind: ProjectKind, readonly projectService: ProjectService, private documentRegistry: DocumentRegistry, hasExplicitListOfFiles: boolean, lastFileExceededProgramSize: string | undefined, private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, protected watchOptions: WatchOptions | undefined, directoryStructureHost: DirectoryStructureHost, currentDirectory: string | undefined, customRealpath?: (s: string) => string) { this.directoryStructureHost = directoryStructureHost; this.currentDirectory = this.projectService.getNormalizedAbsolutePath(currentDirectory || ""); this.getCanonicalFileName = this.projectService.toCanonicalFileName; this.cancellationToken = new ThrottledCancellationToken(this.projectService.cancellationToken, this.projectService.throttleWaitMilliseconds); if (!this.compilerOptions) { this.compilerOptions = getDefaultCompilerOptions(); this.compilerOptions.allowNonTsExtensions = true; this.compilerOptions.allowJs = true; } else if (hasExplicitListOfFiles || this.compilerOptions.allowJs || this.projectService.hasDeferredExtension()) { // If files are listed explicitly or allowJs is specified, allow all extensions this.compilerOptions.allowNonTsExtensions = true; } this.languageServiceEnabled = !projectService.syntaxOnly; this.setInternalCompilerOptionsForEmittingJsFiles(); const host = this.projectService.host; if (this.projectService.logger.loggingEnabled()) { this.trace = s => this.writeLog(s); } else if (host.trace) { this.trace = s => host.trace!(s); } if (host.realpath) { this.realpath = customRealpath || (path => host.realpath!(path)); } // Use the current directory as resolution root only if the project created using current directory string this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory, /*logChangesWhenResolvingModule*/ true); this.languageService = createLanguageService(this, this.documentRegistry, projectService.syntaxOnly); if (lastFileExceededProgramSize) { this.disableLanguageService(lastFileExceededProgramSize); } this.markAsDirty(); this.projectService.pendingEnsureProjectForOpenFiles = true; this.packageJsonCache = createPackageJsonCache(this); } isKnownTypesPackageName(name: string): boolean { return this.typingsCache.isKnownTypesPackageName(name); } installPackage(options: InstallPackageOptions): Promise { return this.typingsCache.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) }); } /*@internal*/ getGlobalTypingsCacheLocation() { return this.getGlobalCache(); } private get typingsCache(): TypingsCache { return this.projectService.typingsCache; } /*@internal*/ getProbableSymlinks(files: readonly SourceFile[]): ReadonlyMap { return this.symlinks || (this.symlinks = discoverProbableSymlinks( files, this.getCanonicalFileName, this.getCurrentDirectory())); } // Method of LanguageServiceHost getCompilationSettings() { return this.compilerOptions; } // Method to support public API getCompilerOptions() { return this.getCompilationSettings(); } getNewLine() { return this.projectService.host.newLine; } getProjectVersion() { return this.projectStateVersion.toString(); } getProjectReferences(): readonly ProjectReference[] | undefined { return undefined; } getScriptFileNames() { if (!this.rootFiles) { return ts.emptyArray; } let result: string[] | undefined; this.rootFilesMap.forEach(value => { if (this.languageServiceEnabled || (value.info && value.info.isScriptOpen())) { // if language service is disabled - process only files that are open (result || (result = [])).push(value.fileName); } }); return addRange(result, this.typingFiles) || ts.emptyArray; } private getOrCreateScriptInfoAndAttachToProject(fileName: string) { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.currentDirectory, this.directoryStructureHost); if (scriptInfo) { const existingValue = this.rootFilesMap.get(scriptInfo.path); if (existingValue && existingValue.info !== scriptInfo) { // This was missing path earlier but now the file exists. Update the root this.rootFiles.push(scriptInfo); existingValue.info = scriptInfo; } scriptInfo.attachToProject(this); } return scriptInfo; } getScriptKind(fileName: string) { const info = this.getOrCreateScriptInfoAndAttachToProject(fileName); return (info && info.scriptKind)!; // TODO: GH#18217 } getScriptVersion(filename: string) { const info = this.getOrCreateScriptInfoAndAttachToProject(filename); return (info && info.getLatestVersion())!; // TODO: GH#18217 } getScriptSnapshot(filename: string): IScriptSnapshot | undefined { const scriptInfo = this.getOrCreateScriptInfoAndAttachToProject(filename); if (scriptInfo) { return scriptInfo.getSnapshot(); } } getCancellationToken(): HostCancellationToken { return this.cancellationToken; } getCurrentDirectory(): string { return this.currentDirectory; } getDefaultLibFileName() { const nodeModuleBinDir = getDirectoryPath(normalizePath(this.projectService.getExecutingFilePath())); return combinePaths(nodeModuleBinDir, getDefaultLibFileName(this.compilerOptions)); } useCaseSensitiveFileNames() { return this.projectService.host.useCaseSensitiveFileNames; } readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { return this.directoryStructureHost.readDirectory!(path, extensions, exclude, include, depth); } readFile(fileName: string): string | undefined { return this.projectService.host.readFile(fileName); } writeFile(fileName: string, content: string): void { return this.projectService.host.writeFile(fileName, content); } fileExists(file: string): boolean { return this.fileExistsWithCache(file); } /* @internal */ fileExistsWithCache(file: string): boolean { // As an optimization, don't hit the disks for files we already know don't exist // (because we're watching for their creation). const path = this.toPath(file); return !this.isWatchedMissingFile(path) && this.directoryStructureHost.fileExists(file); } resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference): (ResolvedModuleFull | undefined)[] { return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference); } getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined { return this.resolutionCache.getResolvedModuleWithFailedLookupLocationsFromCache(moduleName, containingFile); } resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[] { return this.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile, redirectedReference); } directoryExists(path: string): boolean { return this.directoryStructureHost.directoryExists!(path); // TODO: GH#18217 } getDirectories(path: string): string[] { return this.directoryStructureHost.getDirectories!(path); // TODO: GH#18217 } /*@internal*/ getCachedDirectoryStructureHost(): CachedDirectoryStructureHost { return undefined!; // TODO: GH#18217 } /*@internal*/ toPath(fileName: string) { return toPath(fileName, this.currentDirectory, this.projectService.toCanonicalFileName); } /*@internal*/ watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { return this.projectService.watchFactory.watchDirectory( this.projectService.host, directory, cb, flags, this.projectService.getWatchOptions(this), WatchType.FailedLookupLocations, this ); } /*@internal*/ onInvalidatedResolution() { this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } /*@internal*/ watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { return this.projectService.watchFactory.watchDirectory( this.projectService.host, directory, cb, flags, this.projectService.getWatchOptions(this), WatchType.TypeRoots, this ); } /*@internal*/ onChangedAutomaticTypeDirectiveNames() { this.hasChangedAutomaticTypeDirectiveNames = true; this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } /*@internal*/ getGlobalCache() { return this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined; } /*@internal*/ globalCacheResolutionModuleName = JsTyping.nonRelativeModuleNameForTypingCache; /*@internal*/ fileIsOpen(filePath: Path) { return this.projectService.openFiles.has(filePath); } /*@internal*/ writeLog(s: string) { this.projectService.logger.info(s); } log(s: string) { this.writeLog(s); } error(s: string) { this.projectService.logger.msg(s, Msg.Err); } private setInternalCompilerOptionsForEmittingJsFiles() { if (this.projectKind === ProjectKind.Inferred || this.projectKind === ProjectKind.External) { this.compilerOptions.noEmitForJsFiles = true; } } /** * Get the errors that dont have any file name associated */ getGlobalProjectErrors(): readonly Diagnostic[] { return emptyArray; } getAllProjectErrors(): readonly Diagnostic[] { return emptyArray; } getLanguageService(ensureSynchronized = true): LanguageService { if (ensureSynchronized) { updateProjectIfDirty(this); } return this.languageService; } /** @internal */ getSourceMapper(): SourceMapper { return this.getLanguageService().getSourceMapper(); } /*@internal*/ getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { return this.projectService.getDocumentPositionMapper(this, generatedFileName, sourceFileName); } /*@internal*/ getSourceFileLike(fileName: string) { return this.projectService.getSourceFileLike(fileName, this); } /*@internal*/ shouldEmitFile(scriptInfo: ScriptInfo | undefined) { return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent() && !this.program!.isSourceOfProjectReferenceRedirect(scriptInfo.path); } getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { if (!this.languageServiceEnabled) { return []; } updateProjectIfDirty(this); this.builderState = BuilderState.create(this.program!, this.projectService.toCanonicalFileName, this.builderState); return mapDefined(BuilderState.getFilesAffectedBy(this.builderState, this.program!, scriptInfo.path, this.cancellationToken, data => this.projectService.host.createHash!(data)), // TODO: GH#18217 sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } /** * Returns true if emit was conducted */ emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { if (!this.languageServiceEnabled || !this.shouldEmitFile(scriptInfo)) { return false; } const { emitSkipped, outputFiles } = this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(scriptInfo.fileName); if (!emitSkipped) { for (const outputFile of outputFiles) { const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, this.currentDirectory); writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); } } return !emitSkipped; } enableLanguageService() { if (this.languageServiceEnabled || this.projectService.syntaxOnly) { return; } this.languageServiceEnabled = true; this.lastFileExceededProgramSize = undefined; this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ true); } disableLanguageService(lastFileExceededProgramSize?: string) { if (!this.languageServiceEnabled) { return; } Debug.assert(!this.projectService.syntaxOnly); this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; this.lastFileExceededProgramSize = lastFileExceededProgramSize; this.builderState = undefined; this.resolutionCache.closeTypeRootsWatch(); this.clearGeneratedFileWatch(); this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false); } getProjectName() { return this.projectName; } abstract getTypeAcquisition(): TypeAcquisition; protected removeLocalTypingsFromTypeAcquisition(newTypeAcquisition: TypeAcquisition): TypeAcquisition { if (!newTypeAcquisition || !newTypeAcquisition.include) { // Nothing to filter out, so just return as-is return newTypeAcquisition; } return { ...newTypeAcquisition, include: this.removeExistingTypings(newTypeAcquisition.include) }; } getExternalFiles(): SortedReadonlyArray { return sort(flatMap(this.plugins, plugin => { if (typeof plugin.module.getExternalFiles !== "function") return; try { return plugin.module.getExternalFiles(this); } catch (e) { this.projectService.logger.info(`A plugin threw an exception in getExternalFiles: ${e}`); if (e.stack) { this.projectService.logger.info(e.stack); } } })); } getSourceFile(path: Path) { if (!this.program) { return undefined; } return this.program.getSourceFileByPath(path); } /* @internal */ getSourceFileOrConfigFile(path: Path): SourceFile | undefined { const options = this.program!.getCompilerOptions(); return path === options.configFilePath ? options.configFile : this.getSourceFile(path); } close() { if (this.program) { // if we have a program - release all files that are enlisted in program but arent root // The releasing of the roots happens later // The project could have pending update remaining and hence the info could be in the files but not in program graph for (const f of this.program.getSourceFiles()) { this.detachScriptInfoIfNotRoot(f.fileName); } this.program.forEachResolvedProjectReference(ref => { if (ref) { this.detachScriptInfoFromProject(ref.sourceFile.fileName); } }); } // Release external files forEach(this.externalFiles, externalFile => this.detachScriptInfoIfNotRoot(externalFile)); // Always remove root files from the project for (const root of this.rootFiles) { root.detachFromProject(this); } this.projectService.pendingEnsureProjectForOpenFiles = true; this.rootFiles = undefined!; this.rootFilesMap = undefined!; this.externalFiles = undefined!; this.program = undefined!; this.builderState = undefined!; this.resolutionCache.clear(); this.resolutionCache = undefined!; this.cachedUnresolvedImportsPerFile = undefined!; this.directoryStructureHost = undefined!; // Clean up file watchers waiting for missing files if (this.missingFilesMap) { clearMap(this.missingFilesMap, closeFileWatcher); this.missingFilesMap = undefined!; } if (this.packageJsonFilesMap) { clearMap(this.packageJsonFilesMap, closeFileWatcher); this.packageJsonFilesMap = undefined; } this.clearGeneratedFileWatch(); // signal language service to release source files acquired from document registry this.languageService.dispose(); this.languageService = undefined!; } private detachScriptInfoIfNotRoot(uncheckedFilename: string) { const info = this.projectService.getScriptInfo(uncheckedFilename); // We might not find the script info in case its not associated with the project any more // and project graph was not updated (eg delayed update graph in case of files changed/deleted on the disk) if (info && !this.isRoot(info)) { info.detachFromProject(this); } } isClosed() { return this.rootFiles === undefined; } hasRoots() { return this.rootFiles && this.rootFiles.length > 0; } /*@internal*/ isOrphan() { return false; } getRootFiles() { return this.rootFiles && this.rootFiles.map(info => info.fileName); } /*@internal*/ getRootFilesMap() { return this.rootFilesMap; } getRootScriptInfos() { return this.rootFiles; } getScriptInfos(): ScriptInfo[] { if (!this.languageServiceEnabled) { // if language service is not enabled - return just root files return this.rootFiles; } return map(this.program!.getSourceFiles(), sourceFile => { const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.resolvedPath); Debug.assert(!!scriptInfo, "getScriptInfo", () => `scriptInfo for a file '${sourceFile.fileName}' Path: '${sourceFile.path}' / '${sourceFile.resolvedPath}' is missing.`); return scriptInfo!; }); } getExcludedFiles(): readonly NormalizedPath[] { return emptyArray; } getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) { if (!this.program) { return []; } if (!this.languageServiceEnabled) { // if language service is disabled assume that all files in program are root files + default library let rootFiles = this.getRootFiles(); if (this.compilerOptions) { const defaultLibrary = getDefaultLibFilePath(this.compilerOptions); if (defaultLibrary) { (rootFiles || (rootFiles = [])).push(asNormalizedPath(defaultLibrary)); } } return rootFiles; } const result: NormalizedPath[] = []; for (const f of this.program.getSourceFiles()) { if (excludeFilesFromExternalLibraries && this.program.isSourceFileFromExternalLibrary(f)) { continue; } result.push(asNormalizedPath(f.fileName)); } if (!excludeConfigFiles) { const configFile = this.program.getCompilerOptions().configFile; if (configFile) { result.push(asNormalizedPath(configFile.fileName)); if (configFile.extendedSourceFiles) { for (const f of configFile.extendedSourceFiles) { result.push(asNormalizedPath(f)); } } } } return result; } /* @internal */ getFileNamesWithRedirectInfo(includeProjectReferenceRedirectInfo: boolean) { return this.getFileNames().map((fileName): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName, isSourceOfProjectReferenceRedirect: includeProjectReferenceRedirectInfo && this.isSourceOfProjectReferenceRedirect(fileName) })); } hasConfigFile(configFilePath: NormalizedPath) { if (this.program && this.languageServiceEnabled) { const configFile = this.program.getCompilerOptions().configFile; if (configFile) { if (configFilePath === asNormalizedPath(configFile.fileName)) { return true; } if (configFile.extendedSourceFiles) { for (const f of configFile.extendedSourceFiles) { if (configFilePath === asNormalizedPath(f)) { return true; } } } } } return false; } containsScriptInfo(info: ScriptInfo): boolean { if (this.isRoot(info)) return true; if (!this.program) return false; const file = this.program.getSourceFileByPath(info.path); return !!file && file.resolvedPath === info.path; } containsFile(filename: NormalizedPath, requireOpen?: boolean): boolean { const info = this.projectService.getScriptInfoForNormalizedPath(filename); if (info && (info.isScriptOpen() || !requireOpen)) { return this.containsScriptInfo(info); } return false; } isRoot(info: ScriptInfo) { return this.rootFilesMap && this.rootFilesMap.get(info.path)?.info === info; } // add a root file to project addRoot(info: ScriptInfo, fileName?: NormalizedPath) { Debug.assert(!this.isRoot(info)); this.rootFiles.push(info); this.rootFilesMap.set(info.path, { fileName: fileName || info.fileName, info }); info.attachToProject(this); this.markAsDirty(); } // add a root file that doesnt exist on host addMissingFileRoot(fileName: NormalizedPath) { const path = this.projectService.toPath(fileName); this.rootFilesMap.set(path, { fileName }); this.markAsDirty(); } removeFile(info: ScriptInfo, fileExists: boolean, detachFromProject: boolean) { if (this.isRoot(info)) { this.removeRoot(info); } if (fileExists) { // If file is present, just remove the resolutions for the file this.resolutionCache.removeResolutionsOfFile(info.path); } else { this.resolutionCache.invalidateResolutionOfFile(info.path); } this.cachedUnresolvedImportsPerFile.delete(info.path); if (detachFromProject) { info.detachFromProject(this); } this.markAsDirty(); } registerFileUpdate(fileName: string) { (this.updatedFileNames || (this.updatedFileNames = createMap())).set(fileName, true); } /*@internal*/ markFileAsDirty(changedFile: Path) { this.markAsDirty(); if (!this.importSuggestionsCache.isEmpty()) { (this.dirtyFilesForSuggestions || (this.dirtyFilesForSuggestions = createMap())).set(changedFile, true); } } markAsDirty() { if (!this.dirty) { this.projectStateVersion++; this.dirty = true; } } /* @internal */ onFileAddedOrRemoved() { this.hasAddedorRemovedFiles = true; } /** * Updates set of files that contribute to this project * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { perfLogger.logStartUpdateGraph(); this.resolutionCache.startRecordingFilesWithChangedResolutions(); const hasNewProgram = this.updateGraphWorker(); const hasAddedorRemovedFiles = this.hasAddedorRemovedFiles; this.hasAddedorRemovedFiles = false; const changedFiles: readonly Path[] = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray; for (const file of changedFiles) { // delete cached information for changed files this.cachedUnresolvedImportsPerFile.delete(file); } // update builder only if language service is enabled // otherwise tell it to drop its internal state if (this.languageServiceEnabled) { // 1. no changes in structure, no changes in unresolved imports - do nothing // 2. no changes in structure, unresolved imports were changed - collect unresolved imports for all files // (can reuse cached imports for files that were not changed) // 3. new files were added/removed, but compilation settings stays the same - collect unresolved imports for all new/modified files // (can reuse cached imports for files that were not changed) // 4. compilation settings were changed in the way that might affect module resolution - drop all caches and collect all data from the scratch if (hasNewProgram || changedFiles.length) { this.lastCachedUnresolvedImportsList = getUnresolvedImports(this.program!, this.cachedUnresolvedImportsPerFile); } this.projectService.typingsCache.enqueueInstallTypingsForProject(this, this.lastCachedUnresolvedImportsList, hasAddedorRemovedFiles); } else { this.lastCachedUnresolvedImportsList = undefined; } if (hasNewProgram) { this.projectProgramVersion++; } perfLogger.logStopUpdateGraph(); return !hasNewProgram; } /*@internal*/ updateTypingFiles(typingFiles: SortedReadonlyArray) { enumerateInsertsAndDeletes(typingFiles, this.typingFiles, getStringComparer(!this.useCaseSensitiveFileNames()), /*inserted*/ noop, removed => this.detachScriptInfoFromProject(removed) ); this.typingFiles = typingFiles; // Invalidate files with unresolved imports this.resolutionCache.setFilesWithInvalidatedNonRelativeUnresolvedImports(this.cachedUnresolvedImportsPerFile); } /* @internal */ getCurrentProgram(): Program | undefined { return this.program; } protected removeExistingTypings(include: string[]): string[] { const existing = getAutomaticTypeDirectiveNames(this.getCompilerOptions(), this.directoryStructureHost); return include.filter(i => existing.indexOf(i) < 0); } private updateGraphWorker() { const oldProgram = this.program; Debug.assert(!this.isClosed(), "Called update graph worker of closed project"); this.writeLog(`Starting updateGraphWorker: Project: ${this.getProjectName()}`); const start = timestamp(); this.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution(); this.resolutionCache.startCachingPerDirectoryResolution(); this.program = this.languageService.getProgram()!; // TODO: GH#18217 this.dirty = false; this.resolutionCache.finishCachingPerDirectoryResolution(); Debug.assert(oldProgram === undefined || this.program !== undefined); // bump up the version if // - oldProgram is not set - this is a first time updateGraph is called // - newProgram is different from the old program and structure of the old program was not reused. const hasNewProgram = this.program && (!oldProgram || (this.program !== oldProgram && !(oldProgram.structureIsReused! & StructureIsReused.Completely))); this.hasChangedAutomaticTypeDirectiveNames = false; if (hasNewProgram) { if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { const newFile = this.program.getSourceFileByPath(f.resolvedPath); if (!newFile || (f.resolvedPath === f.path && newFile.resolvedPath !== f.path)) { // new program does not contain this file - detach it from the project // - remove resolutions only if the new program doesnt contain source file by the path (not resolvedPath since path is used for resolution) this.detachScriptInfoFromProject(f.fileName, !!this.program.getSourceFileByPath(f.path)); } } oldProgram.forEachResolvedProjectReference((resolvedProjectReference, resolvedProjectReferencePath) => { if (resolvedProjectReference && !this.program!.getResolvedProjectReferenceByPath(resolvedProjectReferencePath)) { this.detachScriptInfoFromProject(resolvedProjectReference.sourceFile.fileName); } }); } // Update the missing file paths watcher updateMissingFilePathsWatch( this.program, this.missingFilesMap || (this.missingFilesMap = createMap()), // Watch the missing files missingFilePath => this.addMissingFileWatcher(missingFilePath) ); if (this.generatedFilesMap) { const outPath = this.compilerOptions.outFile && this.compilerOptions.out; if (isGeneratedFileWatcher(this.generatedFilesMap)) { // --out if (!outPath || !this.isValidGeneratedFileWatcher( removeFileExtension(outPath) + Extension.Dts, this.generatedFilesMap, )) { this.clearGeneratedFileWatch(); } } else { // MultiFile if (outPath) { this.clearGeneratedFileWatch(); } else { this.generatedFilesMap.forEach((watcher, source) => { const sourceFile = this.program!.getSourceFileByPath(source as Path); if (!sourceFile || sourceFile.resolvedPath !== source || !this.isValidGeneratedFileWatcher( getDeclarationEmitOutputFilePathWorker(sourceFile.fileName, this.compilerOptions, this.currentDirectory, this.program!.getCommonSourceDirectory(), this.getCanonicalFileName), watcher )) { closeFileWatcherOf(watcher); (this.generatedFilesMap as Map).delete(source); } }); } } } // Watch the type locations that would be added to program as part of automatic type resolutions if (this.languageServiceEnabled) { this.resolutionCache.updateTypeRootsWatch(); } } if (!this.importSuggestionsCache.isEmpty()) { if (this.hasAddedorRemovedFiles || oldProgram && !oldProgram.structureIsReused) { this.importSuggestionsCache.clear(); } else if (this.dirtyFilesForSuggestions && oldProgram && this.program) { forEachKey(this.dirtyFilesForSuggestions, fileName => { const oldSourceFile = oldProgram.getSourceFile(fileName); const sourceFile = this.program!.getSourceFile(fileName); if (this.sourceFileHasChangedOwnImportSuggestions(oldSourceFile, sourceFile)) { this.importSuggestionsCache.clear(); return true; } }); } } if (this.dirtyFilesForSuggestions) { this.dirtyFilesForSuggestions.clear(); } if (this.hasAddedorRemovedFiles) { this.symlinks = undefined; } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; this.externalFiles = this.getExternalFiles(); enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, getStringComparer(!this.useCaseSensitiveFileNames()), // Ensure a ScriptInfo is created for new external files. This is performed indirectly // by the host for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. inserted => { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost)!; scriptInfo.attachToProject(this); }, removed => this.detachScriptInfoFromProject(removed) ); const elapsed = timestamp() - start; this.projectService.sendUpdateGraphPerformanceEvent(elapsed); this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} Version: ${this.getProjectVersion()} structureChanged: ${hasNewProgram} Elapsed: ${elapsed}ms`); if (this.hasAddedorRemovedFiles) { this.print(); } else if (this.program !== oldProgram) { this.writeLog(`Different program with same set of files:: oldProgram.structureIsReused:: ${oldProgram && oldProgram.structureIsReused}`); } return hasNewProgram; } /*@internal*/ private sourceFileHasChangedOwnImportSuggestions(oldSourceFile: SourceFile | undefined, newSourceFile: SourceFile | undefined) { if (!oldSourceFile && !newSourceFile) { return false; } // Probably shouldn’t get this far, but on the off chance the file was added or removed, // we can’t reliably tell anything about it. if (!oldSourceFile || !newSourceFile) { return true; } Debug.assertEqual(oldSourceFile.fileName, newSourceFile.fileName); // If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node. // Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list. if (this.getTypeAcquisition().enable && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile)) { return true; } // Module agumentation and ambient module changes can add or remove exports available to be auto-imported. // Changes elsewhere in the file can change the *type* of an export in a module augmentation, // but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache. if ( !arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) || !this.ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile) ) { return true; } return false; } /*@internal*/ private ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) { if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) { return false; } let oldFileStatementIndex = -1; let newFileStatementIndex = -1; for (const ambientModuleName of newSourceFile.ambientModuleNames) { const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName; oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1); newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1); if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) { return false; } } return true; } private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean) { const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName); if (scriptInfoToDetach) { scriptInfoToDetach.detachFromProject(this); if (!noRemoveResolution) { this.resolutionCache.removeResolutionsOfFile(scriptInfoToDetach.path); } } } private addMissingFileWatcher(missingFilePath: Path) { const fileWatcher = this.projectService.watchFactory.watchFile( this.projectService.host, missingFilePath, (fileName, eventKind) => { if (isConfiguredProject(this)) { this.getCachedDirectoryStructureHost().addOrDeleteFile(fileName, missingFilePath, eventKind); } if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap!.has(missingFilePath)) { this.missingFilesMap!.delete(missingFilePath); fileWatcher.close(); // When a missing file is created, we should update the graph. this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } }, PollingInterval.Medium, this.projectService.getWatchOptions(this), WatchType.MissingFile, this ); return fileWatcher; } private isWatchedMissingFile(path: Path) { return !!this.missingFilesMap && this.missingFilesMap.has(path); } /* @internal */ addGeneratedFileWatch(generatedFile: string, sourceFile: string) { if (this.compilerOptions.outFile || this.compilerOptions.out) { // Single watcher if (!this.generatedFilesMap) { this.generatedFilesMap = this.createGeneratedFileWatcher(generatedFile); } } else { // Map const path = this.toPath(sourceFile); if (this.generatedFilesMap) { if (isGeneratedFileWatcher(this.generatedFilesMap)) { Debug.fail(`${this.projectName} Expected to not have --out watcher for generated file with options: ${JSON.stringify(this.compilerOptions)}`); return; } if (this.generatedFilesMap.has(path)) return; } else { this.generatedFilesMap = createMap(); } this.generatedFilesMap.set(path, this.createGeneratedFileWatcher(generatedFile)); } } private createGeneratedFileWatcher(generatedFile: string): GeneratedFileWatcher { return { generatedFilePath: this.toPath(generatedFile), watcher: this.projectService.watchFactory.watchFile( this.projectService.host, generatedFile, () => this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this), PollingInterval.High, this.projectService.getWatchOptions(this), WatchType.MissingGeneratedFile, this ) }; } private isValidGeneratedFileWatcher(generateFile: string, watcher: GeneratedFileWatcher) { return this.toPath(generateFile) === watcher.generatedFilePath; } private clearGeneratedFileWatch() { if (this.generatedFilesMap) { if (isGeneratedFileWatcher(this.generatedFilesMap)) { closeFileWatcherOf(this.generatedFilesMap); } else { clearMap(this.generatedFilesMap, closeFileWatcherOf); } this.generatedFilesMap = undefined; } } getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo | undefined { const scriptInfo = this.projectService.getScriptInfoForPath(this.toPath(fileName)); if (scriptInfo && !scriptInfo.isAttached(this)) { return Errors.ThrowProjectDoesNotContainDocument(fileName, this); } return scriptInfo; } getScriptInfo(uncheckedFileName: string) { return this.projectService.getScriptInfo(uncheckedFileName); } filesToString(writeProjectFileNames: boolean) { if (this.isInitialLoadPending()) return "\tFiles (0) InitialLoadPending\n"; if (!this.program) return "\tFiles (0) NoProgram\n"; const sourceFiles = this.program.getSourceFiles(); let strBuilder = `\tFiles (${sourceFiles.length})\n`; if (writeProjectFileNames) { for (const file of sourceFiles) { strBuilder += `\t${file.fileName}\n`; } } return strBuilder; } /*@internal*/ print(counter?: number) { this.writeLog(`Project '${this.projectName}' (${ProjectKind[this.projectKind]}) ${counter === undefined ? "" : counter}`); this.writeLog(this.filesToString(this.projectService.logger.hasLevel(LogLevel.verbose))); this.writeLog("-----------------------------------------------"); } setCompilerOptions(compilerOptions: CompilerOptions) { if (compilerOptions) { compilerOptions.allowNonTsExtensions = true; const oldOptions = this.compilerOptions; this.compilerOptions = compilerOptions; this.setInternalCompilerOptionsForEmittingJsFiles(); if (changesAffectModuleResolution(oldOptions, compilerOptions)) { // reset cached unresolved imports if changes in compiler options affected module resolution this.cachedUnresolvedImportsPerFile.clear(); this.lastCachedUnresolvedImportsList = undefined; this.resolutionCache.clear(); } this.markAsDirty(); } } /*@internal*/ setWatchOptions(watchOptions: WatchOptions | undefined) { this.watchOptions = watchOptions; } /*@internal*/ getWatchOptions(): WatchOptions | undefined { return this.watchOptions; } /* @internal */ getChangesSinceVersion(lastKnownVersion?: number, includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics { const includeProjectReferenceRedirectInfoIfRequested = includeProjectReferenceRedirectInfo ? (files: Map) => arrayFrom(files.entries(), ([fileName, isSourceOfProjectReferenceRedirect]): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName, isSourceOfProjectReferenceRedirect })) : (files: Map) => arrayFrom(files.keys()); // Update the graph only if initial configured project load is not pending if (!this.isInitialLoadPending()) { updateProjectIfDirty(this); } const info: protocol.ProjectVersionInfo = { projectName: this.getProjectName(), version: this.projectProgramVersion, isInferred: isInferredProject(this), options: this.getCompilationSettings(), languageServiceDisabled: !this.languageServiceEnabled, lastFileExceededProgramSize: this.lastFileExceededProgramSize }; const updatedFileNames = this.updatedFileNames; this.updatedFileNames = undefined; // check if requested version is the same that we have reported last time if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { // if current structure version is the same - return info without any changes if (this.projectProgramVersion === this.lastReportedVersion && !updatedFileNames) { return { info, projectErrors: this.getGlobalProjectErrors() }; } // compute and return the difference const lastReportedFileNames = this.lastReportedFileNames; const externalFiles = this.getExternalFiles().map((f): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName: toNormalizedPath(f), isSourceOfProjectReferenceRedirect: false })); const currentFiles = arrayToMap( this.getFileNamesWithRedirectInfo(!!includeProjectReferenceRedirectInfo).concat(externalFiles), info => info.fileName, info => info.isSourceOfProjectReferenceRedirect ); const added: Map = new Map(); const removed: Map = new Map(); const updated: string[] = updatedFileNames ? arrayFrom(updatedFileNames.keys()) : []; const updatedRedirects: protocol.FileWithProjectReferenceRedirectInfo[] = []; forEachEntry(currentFiles, (isSourceOfProjectReferenceRedirect, fileName) => { if (!lastReportedFileNames.has(fileName)) { added.set(fileName, isSourceOfProjectReferenceRedirect); } else if (includeProjectReferenceRedirectInfo && isSourceOfProjectReferenceRedirect !== lastReportedFileNames.get(fileName)){ updatedRedirects.push({ fileName, isSourceOfProjectReferenceRedirect }); } }); forEachEntry(lastReportedFileNames, (isSourceOfProjectReferenceRedirect, fileName) => { if (!currentFiles.has(fileName)) { removed.set(fileName, isSourceOfProjectReferenceRedirect); } }); this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectProgramVersion; return { info, changes: { added: includeProjectReferenceRedirectInfoIfRequested(added), removed: includeProjectReferenceRedirectInfoIfRequested(removed), updated: includeProjectReferenceRedirectInfo ? updated.map((fileName): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName, isSourceOfProjectReferenceRedirect: this.isSourceOfProjectReferenceRedirect(fileName) })) : updated, updatedRedirects: includeProjectReferenceRedirectInfo ? updatedRedirects : undefined }, projectErrors: this.getGlobalProjectErrors() }; } else { // unknown version - return everything const projectFileNames = this.getFileNamesWithRedirectInfo(!!includeProjectReferenceRedirectInfo); const externalFiles = this.getExternalFiles().map((f): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName: toNormalizedPath(f), isSourceOfProjectReferenceRedirect: false })); const allFiles = projectFileNames.concat(externalFiles); this.lastReportedFileNames = arrayToMap( allFiles, info => info.fileName, info => info.isSourceOfProjectReferenceRedirect ); this.lastReportedVersion = this.projectProgramVersion; return { info, files: includeProjectReferenceRedirectInfo ? allFiles : allFiles.map(f => f.fileName), projectErrors: this.getGlobalProjectErrors() }; } } // remove a root file from project protected removeRoot(info: ScriptInfo): void { orderedRemoveItem(this.rootFiles, info); this.rootFilesMap.delete(info.path); } /*@internal*/ isSourceOfProjectReferenceRedirect(fileName: string) { return !!this.program && this.program.isSourceOfProjectReferenceRedirect(fileName); } protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; if (!host.require) { this.projectService.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded"); return; } // Search any globally-specified probe paths, then our peer node_modules const searchPaths = [ ...this.projectService.pluginProbeLocations, // ../../.. to walk from X/node_modules/typescript/lib/tsserver.js to X/node_modules/ combinePaths(this.projectService.getExecutingFilePath(), "../../.."), ]; if (this.projectService.globalPlugins) { // Enable global plugins with synthetic configuration entries for (const globalPluginName of this.projectService.globalPlugins) { // Skip empty names from odd commandline parses if (!globalPluginName) continue; // Skip already-locally-loaded plugins if (options.plugins && options.plugins.some(p => p.name === globalPluginName)) continue; // Provide global: true so plugins can detect why they can't find their config this.projectService.logger.info(`Loading global plugin ${globalPluginName}`); this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths, pluginConfigOverrides); } } } protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined) { this.projectService.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`); const log = (message: string) => this.projectService.logger.info(message); let errorLogs: string[] | undefined; const logError = (message: string) => { (errorLogs || (errorLogs = [])).push(message); }; const resolvedModule = firstDefined(searchPaths, searchPath => Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log, logError)); if (resolvedModule) { const configurationOverride = pluginConfigOverrides && pluginConfigOverrides.get(pluginConfigEntry.name); if (configurationOverride) { // Preserve the name property since it's immutable const pluginName = pluginConfigEntry.name; pluginConfigEntry = configurationOverride; pluginConfigEntry.name = pluginName; } this.enableProxy(resolvedModule, pluginConfigEntry); } else { forEach(errorLogs, log); this.projectService.logger.info(`Couldn't find ${pluginConfigEntry.name}`); } } private enableProxy(pluginModuleFactory: PluginModuleFactory, configEntry: PluginImport) { try { if (typeof pluginModuleFactory !== "function") { this.projectService.logger.info(`Skipped loading plugin ${configEntry.name} because it did not expose a proper factory function`); return; } const info: PluginCreateInfo = { config: configEntry, project: this, languageService: this.languageService, languageServiceHost: this, serverHost: this.projectService.host }; const pluginModule = pluginModuleFactory({ typescript: ts }); const newLS = pluginModule.create(info); for (const k of Object.keys(this.languageService)) { // eslint-disable-next-line no-in-operator if (!(k in newLS)) { this.projectService.logger.info(`Plugin activation warning: Missing proxied method ${k} in created LS. Patching.`); (newLS as any)[k] = (this.languageService as any)[k]; } } this.projectService.logger.info(`Plugin validation succeded`); this.languageService = newLS; this.plugins.push({ name: configEntry.name, module: pluginModule }); } catch (e) { this.projectService.logger.info(`Plugin activation failed: ${e}`); } } /*@internal*/ onPluginConfigurationChanged(pluginName: string, configuration: any) { this.plugins.filter(plugin => plugin.name === pluginName).forEach(plugin => { if (plugin.module.onConfigurationChanged) { plugin.module.onConfigurationChanged(configuration); } }); } /** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */ refreshDiagnostics() { this.projectService.sendProjectsUpdatedInBackgroundEvent(); } /*@internal*/ getPackageJsonsVisibleToFile(fileName: string, rootDir?: string): readonly PackageJsonInfo[] { const packageJsonCache = this.packageJsonCache; const watchPackageJsonFile = this.watchPackageJsonFile.bind(this); const toPath = this.toPath.bind(this); const rootPath = rootDir && toPath(rootDir); const filePath = toPath(fileName); const result: PackageJsonInfo[] = []; forEachAncestorDirectory(getDirectoryPath(filePath), function processDirectory(directory): boolean | undefined { switch (packageJsonCache.directoryHasPackageJson(directory)) { // Sync and check same directory again case Ternary.Maybe: packageJsonCache.searchDirectoryAndAncestors(directory); return processDirectory(directory); // Check package.json case Ternary.True: const packageJsonFileName = combinePaths(directory, "package.json"); watchPackageJsonFile(packageJsonFileName); const info = packageJsonCache.getInDirectory(directory); if (info) result.push(info); } if (rootPath && rootPath === toPath(directory)) { return true; } }); return result; } /*@internal*/ onAddPackageJson(path: Path) { this.packageJsonCache.addOrUpdate(path); this.watchPackageJsonFile(path); } /*@internal*/ getImportSuggestionsCache() { return this.importSuggestionsCache; } private watchPackageJsonFile(path: Path) { const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = createMap()); if (!watchers.has(path)) { watchers.set(path, this.projectService.watchFactory.watchFile( this.projectService.host, path, (fileName, eventKind) => { const path = this.toPath(fileName); switch (eventKind) { case FileWatcherEventKind.Created: return Debug.fail(); case FileWatcherEventKind.Changed: this.packageJsonCache.addOrUpdate(path); break; case FileWatcherEventKind.Deleted: this.packageJsonCache.delete(path); watchers.get(path)!.close(); watchers.delete(path); } }, PollingInterval.Low, this.projectService.getWatchOptions(this), WatchType.PackageJsonFile, )); } } } function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: Map): SortedReadonlyArray { const ambientModules = program.getTypeChecker().getAmbientModules().map(mod => stripQuotes(mod.getName())); return sortAndDeduplicate(flatMap(program.getSourceFiles(), sourceFile => extractUnresolvedImportsFromSourceFile(sourceFile, ambientModules, cachedUnresolvedImportsPerFile))); } function extractUnresolvedImportsFromSourceFile(file: SourceFile, ambientModules: readonly string[], cachedUnresolvedImportsPerFile: Map): readonly string[] { return getOrUpdate(cachedUnresolvedImportsPerFile, file.path, () => { if (!file.resolvedModules) return emptyArray; let unresolvedImports: string[] | undefined; file.resolvedModules.forEach((resolvedModule, name) => { // pick unresolved non-relative names if ((!resolvedModule || !resolutionExtensionIsTSOrJson(resolvedModule.extension)) && !isExternalModuleNameRelative(name) && !ambientModules.some(m => m === name)) { unresolvedImports = append(unresolvedImports, parsePackageName(name).packageName); } }); return unresolvedImports || emptyArray; }); } /** * If a file is opened and no tsconfig (or jsconfig) is found, * the file and its imports/references are put into an InferredProject. */ export class InferredProject extends Project { private static readonly newName = (() => { let nextId = 1; return () => { const id = nextId; nextId++; return makeInferredProjectName(id); }; })(); private _isJsInferredProject = false; toggleJsInferredProject(isJsInferredProject: boolean) { if (isJsInferredProject !== this._isJsInferredProject) { this._isJsInferredProject = isJsInferredProject; this.setCompilerOptions(); } } setCompilerOptions(options?: CompilerOptions) { // Avoid manipulating the given options directly if (!options && !this.getCompilationSettings()) { return; } const newOptions = cloneCompilerOptions(options || this.getCompilationSettings()); if (this._isJsInferredProject && typeof newOptions.maxNodeModuleJsDepth !== "number") { newOptions.maxNodeModuleJsDepth = 2; } else if (!this._isJsInferredProject) { newOptions.maxNodeModuleJsDepth = undefined; } newOptions.allowJs = true; super.setCompilerOptions(newOptions); } /** this is canonical project root path */ readonly projectRootPath: string | undefined; /*@internal*/ /** stored only if their is no projectRootPath and this isnt single inferred project */ readonly canonicalCurrentDirectory: string | undefined; /*@internal*/ constructor( projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, watchOptions: WatchOptions | undefined, projectRootPath: NormalizedPath | undefined, currentDirectory: string | undefined, pluginConfigOverrides: Map | undefined) { super(InferredProject.newName(), ProjectKind.Inferred, projectService, documentRegistry, // TODO: GH#18217 /*files*/ undefined!, /*lastFileExceededProgramSize*/ undefined, compilerOptions, /*compileOnSaveEnabled*/ false, watchOptions, projectService.host, currentDirectory); this.projectRootPath = projectRootPath && projectService.toCanonicalFileName(projectRootPath); if (!projectRootPath && !projectService.useSingleInferredProject) { this.canonicalCurrentDirectory = projectService.toCanonicalFileName(this.currentDirectory); } this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides); } addRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); this.projectService.startWatchingConfigFilesForInferredProjectRoot(info); if (!this._isJsInferredProject && info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } super.addRoot(info); } removeRoot(info: ScriptInfo) { this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info); super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { if (every(this.getRootScriptInfos(), rootInfo => !rootInfo.isJavaScript())) { this.toggleJsInferredProject(/*isJsInferredProject*/ false); } } } /*@internal*/ isOrphan() { return !this.hasRoots(); } isProjectWithSingleRoot() { // - when useSingleInferredProject is not set and projectRootPath is not set, // we can guarantee that this will be the only root // - other wise it has single root if it has single root script info return (!this.projectRootPath && !this.projectService.useSingleInferredProject) || this.getRootScriptInfos().length === 1; } close() { forEach(this.getRootScriptInfos(), info => this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info)); super.close(); } getTypeAcquisition(): TypeAcquisition { return { enable: allRootFilesAreJsOrDts(this), include: [], exclude: [] }; } } /*@internal*/ interface SymlinkedDirectory { real: string; realPath: Path; } /** * If a file is opened, the server will look for a tsconfig (or jsconfig) * and if successful create a ConfiguredProject for it. * Otherwise it will create an InferredProject. */ export class ConfiguredProject extends Project { private typeAcquisition!: TypeAcquisition; // TODO: GH#18217 /* @internal */ configFileWatcher: FileWatcher | undefined; private directoriesWatchedForWildcards: Map | undefined; readonly canonicalConfigFilePath: NormalizedPath; private projectReferenceCallbacks: ResolvedProjectReferenceCallbacks | undefined; private mapOfDeclarationDirectories: Map | undefined; private symlinkedDirectories: Map | undefined; private symlinkedFiles: Map | undefined; /* @internal */ pendingReload: ConfigFileProgramReloadLevel | undefined; /* @internal */ pendingReloadReason: string | undefined; /* @internal */ openFileWatchTriggered = createMap(); /*@internal*/ configFileSpecs: ConfigFileSpecs | undefined; /*@internal*/ canConfigFileJsonReportNoInputFiles = false; /** Ref count to the project when opened from external project */ private externalProjectRefCount = 0; private projectErrors: Diagnostic[] | undefined; private projectReferences: readonly ProjectReference[] | undefined; /** Potential project references before the project is actually loaded (read config file) */ /*@internal*/ potentialProjectReferences: Map | undefined; /*@internal*/ projectOptions?: ProjectOptions | true; /*@internal*/ isInitialLoadPending: () => boolean = returnTrue; /*@internal*/ sendLoadingProjectFinish = false; /*@internal*/ constructor(configFileName: NormalizedPath, projectService: ProjectService, documentRegistry: DocumentRegistry, cachedDirectoryStructureHost: CachedDirectoryStructureHost) { super(configFileName, ProjectKind.Configured, projectService, documentRegistry, /*hasExplicitListOfFiles*/ false, /*lastFileExceededProgramSize*/ undefined, /*compilerOptions*/ {}, /*compileOnSaveEnabled*/ false, /*watchOptions*/ undefined, cachedDirectoryStructureHost, getDirectoryPath(configFileName), projectService.host.realpath && (s => this.getRealpath(s)) ); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); } /* @internal */ setResolvedProjectReferenceCallbacks(projectReferenceCallbacks: ResolvedProjectReferenceCallbacks) { this.projectReferenceCallbacks = projectReferenceCallbacks; } /* @internal */ useSourceOfProjectReferenceRedirect = () => !!this.languageServiceEnabled && !this.getCompilerOptions().disableSourceOfProjectReferenceRedirect; private fileExistsIfProjectReferenceDts(file: string) { const source = this.projectReferenceCallbacks!.getSourceOfProjectReferenceRedirect(file); return source !== undefined ? isString(source) ? super.fileExists(source) : true : undefined; } /** * This implementation of fileExists checks if the file being requested is * .d.ts file for the referenced Project. * If it is it returns true irrespective of whether that file exists on host */ fileExists(file: string): boolean { if (super.fileExists(file)) return true; if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false; if (!isDeclarationFileName(file)) return false; // Project references go to source file instead of .d.ts file return this.fileOrDirectoryExistsUsingSource(file, /*isFile*/ true); } private directoryExistsIfProjectReferenceDeclDir(dir: string) { const dirPath = this.toPath(dir); const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`; return forEachKey( this.mapOfDeclarationDirectories!, declDirPath => dirPath === declDirPath || // Any parent directory of declaration dir startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) || // Any directory inside declaration dir startsWith(dirPath, `${declDirPath}/`) ); } /** * This implementation of directoryExists checks if the directory being requested is * directory of .d.ts file for the referenced Project. * If it is it returns true irrespective of whether that directory exists on host */ directoryExists(path: string): boolean { if (super.directoryExists(path)) { this.handleDirectoryCouldBeSymlink(path); return true; } if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false; if (!this.mapOfDeclarationDirectories) { this.mapOfDeclarationDirectories = createMap(); this.projectReferenceCallbacks.forEachResolvedProjectReference(ref => { if (!ref) return; const out = ref.commandLine.options.outFile || ref.commandLine.options.out; if (out) { this.mapOfDeclarationDirectories!.set(getDirectoryPath(this.toPath(out)), true); } else { // Set declaration's in different locations only, if they are next to source the directory present doesnt change const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir; if (declarationDir) { this.mapOfDeclarationDirectories!.set(this.toPath(declarationDir), true); } } }); } return this.fileOrDirectoryExistsUsingSource(path, /*isFile*/ false); } /** * Call super.getDirectories only if directory actually present on the host * This is needed to ensure that we arent getting directories that we fake about presence for */ getDirectories(path: string): string[] { return !this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks || super.directoryExists(path) ? super.getDirectories(path) : []; } private realpathIfSymlinkedProjectReferenceDts(s: string): string | undefined { return this.symlinkedFiles && this.symlinkedFiles.get(this.toPath(s)); } private getRealpath(s: string): string { return this.realpathIfSymlinkedProjectReferenceDts(s) || this.projectService.host.realpath!(s); } private handleDirectoryCouldBeSymlink(directory: string) { if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return; // Because we already watch node_modules, handle symlinks in there if (!this.realpath || !stringContains(directory, nodeModulesPathPart)) return; if (!this.symlinkedDirectories) this.symlinkedDirectories = createMap(); const directoryPath = ensureTrailingDirectorySeparator(this.toPath(directory)); if (this.symlinkedDirectories.has(directoryPath)) return; const real = normalizePath(this.projectService.host.realpath!(directory)); let realPath: Path; if (real === directory || (realPath = ensureTrailingDirectorySeparator(this.toPath(real))) === directoryPath) { // not symlinked this.symlinkedDirectories.set(directoryPath, false); return; } this.symlinkedDirectories.set(directoryPath, { real: ensureTrailingDirectorySeparator(real), realPath }); } private fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean { const fileOrDirectoryExistsUsingSource = isFile ? (file: string) => this.fileExistsIfProjectReferenceDts(file) : (dir: string) => this.directoryExistsIfProjectReferenceDeclDir(dir); // Check current directory or file const result = fileOrDirectoryExistsUsingSource(fileOrDirectory); if (result !== undefined) return result; if (!this.symlinkedDirectories) return false; const fileOrDirectoryPath = this.toPath(fileOrDirectory); if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false; if (isFile && this.symlinkedFiles && this.symlinkedFiles.has(fileOrDirectoryPath)) return true; // If it contains node_modules check if its one of the symlinked path we know of return firstDefinedIterator( this.symlinkedDirectories.entries(), ([directoryPath, symlinkedDirectory]) => { if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined; const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath)); if (isFile && result) { if (!this.symlinkedFiles) this.symlinkedFiles = createMap(); // Store the real path for the file' const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, this.currentDirectory); this.symlinkedFiles.set( fileOrDirectoryPath, `${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}` ); } return result; } ) || false; } /* @internal */ setWatchOptions(watchOptions: WatchOptions | undefined) { const oldOptions = this.getWatchOptions(); super.setWatchOptions(watchOptions); // If watch options different than older options if (this.isInitialLoadPending() && !isJsonEqual(oldOptions, this.getWatchOptions())) { const oldWatcher = this.configFileWatcher; this.createConfigFileWatcher(); if (oldWatcher) oldWatcher.close(); } } /* @internal */ createConfigFileWatcher() { this.configFileWatcher = this.projectService.watchFactory.watchFile( this.projectService.host, this.getConfigFilePath(), (_fileName, eventKind) => this.projectService.onConfigChangedForConfiguredProject(this, eventKind), PollingInterval.High, this.projectService.getWatchOptions(this), WatchType.ConfigFile, this ); } /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { this.isInitialLoadPending = returnFalse; const reloadLevel = this.pendingReload; this.pendingReload = ConfigFileProgramReloadLevel.None; this.projectReferenceCallbacks = undefined; this.mapOfDeclarationDirectories = undefined; this.symlinkedDirectories = undefined; this.symlinkedFiles = undefined; let result: boolean; switch (reloadLevel) { case ConfigFileProgramReloadLevel.Partial: this.openFileWatchTriggered.clear(); result = this.projectService.reloadFileNamesOfConfiguredProject(this); break; case ConfigFileProgramReloadLevel.Full: this.openFileWatchTriggered.clear(); const reason = Debug.assertDefined(this.pendingReloadReason); this.pendingReloadReason = undefined; this.projectService.reloadConfiguredProject(this, reason); result = true; break; default: result = super.updateGraph(); } this.projectService.sendProjectLoadingFinishEvent(this); this.projectService.sendProjectTelemetry(this); return result; } /*@internal*/ getCachedDirectoryStructureHost() { return this.directoryStructureHost as CachedDirectoryStructureHost; } getConfigFilePath() { return asNormalizedPath(this.getProjectName()); } getProjectReferences(): readonly ProjectReference[] | undefined { return this.projectReferences; } updateReferences(refs: readonly ProjectReference[] | undefined) { this.projectReferences = refs; this.potentialProjectReferences = undefined; } /*@internal*/ setPotentialProjectReference(canonicalConfigPath: NormalizedPath) { Debug.assert(this.isInitialLoadPending()); (this.potentialProjectReferences || (this.potentialProjectReferences = createMap())).set(canonicalConfigPath, true); } /*@internal*/ getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined { const program = this.getCurrentProgram(); return program && program.getResolvedProjectReferenceToRedirect(fileName); } /*@internal*/ enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; if (!host.require) { this.projectService.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded"); return; } // Search our peer node_modules, then any globally-specified probe paths // ../../.. to walk from X/node_modules/typescript/lib/tsserver.js to X/node_modules/ const searchPaths = [combinePaths(this.projectService.getExecutingFilePath(), "../../.."), ...this.projectService.pluginProbeLocations]; if (this.projectService.allowLocalPluginLoads) { const local = getDirectoryPath(this.canonicalConfigFilePath); this.projectService.logger.info(`Local plugin loading enabled; adding ${local} to search paths`); searchPaths.unshift(local); } // Enable tsconfig-specified plugins if (options.plugins) { for (const pluginConfigEntry of options.plugins) { this.enablePlugin(pluginConfigEntry, searchPaths, pluginConfigOverrides); } } this.enableGlobalPlugins(options, pluginConfigOverrides); } /** * Get the errors that dont have any file name associated */ getGlobalProjectErrors(): readonly Diagnostic[] { return filter(this.projectErrors, diagnostic => !diagnostic.file) || emptyArray; } /** * Get all the project errors */ getAllProjectErrors(): readonly Diagnostic[] { return this.projectErrors || emptyArray; } setProjectErrors(projectErrors: Diagnostic[]) { this.projectErrors = projectErrors; } setTypeAcquisition(newTypeAcquisition: TypeAcquisition): void { this.typeAcquisition = this.removeLocalTypingsFromTypeAcquisition(newTypeAcquisition); } getTypeAcquisition() { return this.typeAcquisition; } /*@internal*/ watchWildcards(wildcardDirectories: Map) { updateWatchingWildcardDirectories( this.directoriesWatchedForWildcards || (this.directoriesWatchedForWildcards = createMap()), wildcardDirectories, // Create new directory watcher (directory, flags) => this.projectService.watchWildcardDirectory(directory as Path, flags, this), ); } /*@internal*/ stopWatchingWildCards() { if (this.directoriesWatchedForWildcards) { clearMap(this.directoriesWatchedForWildcards, closeFileWatcherOf); this.directoriesWatchedForWildcards = undefined; } } close() { if (this.configFileWatcher) { this.configFileWatcher.close(); this.configFileWatcher = undefined; } this.stopWatchingWildCards(); this.projectErrors = undefined; this.configFileSpecs = undefined; this.projectReferenceCallbacks = undefined; this.mapOfDeclarationDirectories = undefined; this.symlinkedDirectories = undefined; this.symlinkedFiles = undefined; this.openFileWatchTriggered.clear(); super.close(); } /* @internal */ addExternalProjectReference() { this.externalProjectRefCount++; } /* @internal */ deleteExternalProjectReference() { this.externalProjectRefCount--; } /** Returns true if the project is needed by any of the open script info/external project */ /* @internal */ hasOpenRef() { if (!!this.externalProjectRefCount) { return true; } // Closed project doesnt have any reference if (this.isClosed()) { return false; } const configFileExistenceInfo = this.projectService.getConfigFileExistenceInfo(this); if (this.projectService.hasPendingProjectUpdate(this)) { // If there is pending update for this project, // we dont know if this project would be needed by any of the open files impacted by this config file // In that case keep the project alive if there are open files impacted by this project return !!configFileExistenceInfo.openFilesImpactedByConfigFile.size; } // If there is no pending update for this project, // We know exact set of open files that get impacted by this configured project as the files in the project // The project is referenced only if open files impacted by this project are present in this project return forEachEntry( configFileExistenceInfo.openFilesImpactedByConfigFile, (_value, infoPath) => this.containsScriptInfo(this.projectService.getScriptInfoForPath(infoPath as Path)!) ) || false; } /*@internal*/ hasExternalProjectRef() { return !!this.externalProjectRefCount; } getEffectiveTypeRoots() { return getEffectiveTypeRoots(this.getCompilationSettings(), this.directoryStructureHost) || []; } /*@internal*/ updateErrorOnNoInputFiles(fileNameResult: ExpandResult) { updateErrorForNoInputFiles(fileNameResult, this.getConfigFilePath(), this.configFileSpecs!, this.projectErrors!, this.canConfigFileJsonReportNoInputFiles); } } /** * Project whose configuration is handled externally, such as in a '.csproj'. * These are created only if a host explicitly calls `openExternalProject`. */ export class ExternalProject extends Project { excludedFiles: readonly NormalizedPath[] = []; private typeAcquisition!: TypeAcquisition; // TODO: GH#18217 /*@internal*/ constructor(public externalProjectName: string, projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, lastFileExceededProgramSize: string | undefined, public compileOnSaveEnabled: boolean, projectFilePath?: string, pluginConfigOverrides?: Map, watchOptions?: WatchOptions) { super(externalProjectName, ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, lastFileExceededProgramSize, compilerOptions, compileOnSaveEnabled, watchOptions, projectService.host, getDirectoryPath(projectFilePath || normalizeSlashes(externalProjectName))); this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides); } updateGraph() { const result = super.updateGraph(); this.projectService.sendProjectTelemetry(this); return result; } getExcludedFiles() { return this.excludedFiles; } getTypeAcquisition() { return this.typeAcquisition; } setTypeAcquisition(newTypeAcquisition: TypeAcquisition): void { Debug.assert(!!newTypeAcquisition, "newTypeAcquisition may not be null/undefined"); Debug.assert(!!newTypeAcquisition.include, "newTypeAcquisition.include may not be null/undefined"); Debug.assert(!!newTypeAcquisition.exclude, "newTypeAcquisition.exclude may not be null/undefined"); Debug.assert(typeof newTypeAcquisition.enable === "boolean", "newTypeAcquisition.enable may not be null/undefined"); this.typeAcquisition = this.removeLocalTypingsFromTypeAcquisition(newTypeAcquisition); } } /* @internal */ export function isInferredProject(project: Project): project is InferredProject { return project.projectKind === ProjectKind.Inferred; } /* @internal */ export function isConfiguredProject(project: Project): project is ConfiguredProject { return project.projectKind === ProjectKind.Configured; } /* @internal */ export function isExternalProject(project: Project): project is ExternalProject { return project.projectKind === ProjectKind.External; } }