import * as ts from "./_namespaces/ts.js"; import { addRange, append, ApplyCodeActionCommandResult, arrayFrom, arrayIsEqualTo, arrayToMap, BuilderState, CachedDirectoryStructureHost, canJsonReportNoInputFiles, canWatchDirectoryOrFilePath, changeExtension, changesAffectModuleResolution, clearMap, cloneCompilerOptions, closeFileWatcher, closeFileWatcherOf, combinePaths, comparePaths, CompilerHost, CompilerOptions, containsPath, createCacheableExportInfoMap, createLanguageService, createResolutionCache, createSymlinkCache, Debug, Diagnostic, directorySeparator, DirectoryStructureHost, DirectoryWatcherCallback, DocumentPositionMapper, ensureTrailingDirectorySeparator, enumerateInsertsAndDeletes, every, explainFiles, ExportInfoMap, Extension, fileExtensionIs, FileReference, FileWatcher, FileWatcherCallback, FileWatcherEventKind, filter, flatMap, forEach, forEachEntry, forEachKey, generateDjb2Hash, getAllowJSCompilerOption, getAutomaticTypeDirectiveNames, getBaseFileName, GetCanonicalFileName, getCommonSourceDirectoryOfConfig, getDeclarationEmitOutputFilePathWorker, getDefaultCompilerOptions, getDefaultLibFileName, getDefaultLibFilePath, getDirectoryPath, getEffectiveTypeRoots, getEmitDeclarations, getEntrypointsFromPackageJsonInfo, getNormalizedAbsolutePath, getOrUpdate, getOutputDeclarationFileName, GetPackageJsonEntrypointsHost, getStringComparer, HasInvalidatedLibResolutions, HasInvalidatedResolutions, HostCancellationToken, IncompleteCompletionsCache, inferredTypesContainingFile, InstallPackageOptions, IScriptSnapshot, isDeclarationFileName, isExternalModuleNameRelative, isInsideNodeModules, JSDocParsingMode, JsTyping, LanguageService, LanguageServiceHost, LanguageServiceMode, map, mapDefined, maybeBind, memoize, ModuleResolutionCache, ModuleResolutionHost, ModuleSpecifierCache, noop, noopFileWatcher, normalizePath, normalizeSlashes, PackageJsonAutoImportPreference, PackageJsonInfo, ParsedCommandLine, parsePackageName, Path, PerformanceEvent, PluginImport, PollingInterval, Program, ProgramUpdateLevel, ProjectPackageJsonInfo, ProjectReference, removeFileExtension, ResolutionCache, resolutionExtensionIsTSOrJson, ResolvedModuleWithFailedLookupLocations, ResolvedProjectReference, ResolvedRefAndOutputDts, ResolvedTypeReferenceDirectiveWithFailedLookupLocations, resolvePackageNameToPackageJson, returnFalse, ScriptKind, some, sortAndDeduplicate, SortedReadonlyArray, SourceFile, SourceFileLike, SourceMapper, startsWith, StringLiteralLike, stripQuotes, StructureIsReused, SymlinkCache, ThrottledCancellationToken, timestamp, toPath, toSorted, tracing, TypeAcquisition, updateErrorForNoInputFiles, updateMissingFilePathsWatch, WatchDirectoryFlags, WatchOptions, WatchType, } from "./_namespaces/ts.js"; import { ActionInvalidate, asNormalizedPath, createModuleSpecifierCache, emptyArray, Errors, FileStats, getDetailWatchInfo, LogLevel, ModuleImportResult, Msg, NormalizedPath, nullTypingsInstaller, PackageJsonCache, PackageJsonWatcher, ProjectOptions, ProjectService, ScriptInfo, ServerHost, Session, toNormalizedPath, updateProjectIfDirty, } from "./_namespaces/ts.server.js"; import * as protocol from "./protocol.js"; export enum ProjectKind { Inferred, Configured, External, AutoImportProvider, Auxiliary, } /** @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.textStorage.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 (isDeclarationFileName(info.fileName)) { 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) && !isDeclarationFileName(fileName)) || 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; session?: Session; config: any; } export interface PluginModule { create(createInfo: PluginCreateInfo): LanguageService; getExternalFiles?(proj: Project, updateLevel: ProgramUpdateLevel): string[]; onConfigurationChanged?(config: any): void; } export interface PluginModuleWithName { name: string; module: PluginModule; } export type PluginModuleFactory = (mod: { typescript: typeof ts; }) => PluginModule; /** @internal */ export interface PluginImportResult { pluginConfigEntry: PluginImport; resolvedModule: T | undefined; errorLogs: string[] | undefined; } /** @internal */ export type BeginEnablePluginResult = PluginImportResult; /** * The project root can be script info - if root is present, * or it could be just normalized path if root wasn't 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; } /** @internal */ export interface EmitResult { emitSkipped: boolean; diagnostics: readonly Diagnostic[]; } const enum TypingWatcherType { FileWatcher = "FileWatcher", DirectoryWatcher = "DirectoryWatcher", } type TypingWatchers = Map & { isInvoked?: boolean; }; interface TypingsCacheEntry { readonly typeAcquisition: TypeAcquisition; readonly compilerOptions: CompilerOptions; readonly unresolvedImports: SortedReadonlyArray | undefined; } function setIsEqualTo(arr1: string[] | undefined, arr2: string[] | undefined): boolean { if (arr1 === arr2) { return true; } if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) { return true; } const set = new Map(); let unique = 0; for (const v of arr1!) { if (set.get(v) !== true) { set.set(v, true); unique++; } } for (const v of arr2!) { const isSet = set.get(v); if (isSet === undefined) { return false; } if (isSet === true) { set.set(v, false); unique--; } } return unique === 0; } function typeAcquisitionChanged(opt1: TypeAcquisition, opt2: TypeAcquisition): boolean { return opt1.enable !== opt2.enable || !setIsEqualTo(opt1.include, opt2.include) || !setIsEqualTo(opt1.exclude, opt2.exclude); } function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean { // TODO: add more relevant properties return getAllowJSCompilerOption(opt1) !== getAllowJSCompilerOption(opt2); } function unresolvedImportsChanged(imports1: SortedReadonlyArray | undefined, imports2: SortedReadonlyArray | undefined): boolean { if (imports1 === imports2) { return false; } return !arrayIsEqualTo(imports1, imports2); } export abstract class Project implements LanguageServiceHost, ModuleResolutionHost { private rootFilesMap = new Map(); private program: Program | undefined; private externalFiles: SortedReadonlyArray | undefined; private missingFilesMap: Map | undefined; private generatedFilesMap: GeneratedFileWatcherMap | undefined; /** @internal */ protected readonly plugins: PluginModuleWithName[] = []; /** * 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 * * @internal */ cachedUnresolvedImportsPerFile: Map = new Map(); /** @internal */ lastCachedUnresolvedImportsList: SortedReadonlyArray | undefined; private hasAddedorRemovedFiles = false; private hasAddedOrRemovedSymlinks = false; /** @internal */ 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 */ hasInvalidatedResolutions?: HasInvalidatedResolutions | undefined; /** @internal */ hasInvalidatedLibResolutions: HasInvalidatedLibResolutions | undefined; /** @internal */ resolutionCache: ResolutionCache; private builderState: BuilderState | undefined; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ private updatedFileNames: Set | 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 * @internal */ 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 * @internal */ projectStateVersion = 0; protected projectErrors: Diagnostic[] | undefined; /** @internal */ initialLoadPending = false; /** @internal */ dirty = false; /** @internal */ typingFiles: SortedReadonlyArray = emptyArray; private typingsCache: TypingsCacheEntry | undefined; private typingWatchers: TypingWatchers | undefined; /** @internal */ originalConfiguredProjects: Set | undefined; /** @internal */ packageJsonWatches: Set | undefined; /** @internal */ noDtsResolutionProject?: AuxiliaryProject | undefined; /** @internal */ getRedirectFromSourceFile(_fileName: string): ResolvedRefAndOutputDts | undefined { return undefined; } /** @internal */ useSourceOfProjectReferenceRedirect?(): boolean; /** @internal */ getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; private readonly cancellationToken: ThrottledCancellationToken; public isNonTsProject(): boolean { updateProjectIfDirty(this); return allFilesAreJsOrDts(this); } public isJsOnlyProject(): boolean { updateProjectIfDirty(this); return hasOneOrMoreJsAndNoTsFiles(this); } public static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void): {} | undefined { return Project.importServicePluginSync({ name: moduleName }, [initialDir], host, log).resolvedModule; } /** @internal */ public static importServicePluginSync( pluginConfigEntry: PluginImport, searchPaths: string[], host: ServerHost, log: (message: string) => void, ): PluginImportResult { Debug.assertIsDefined(host.require); let errorLogs: string[] | undefined; let resolvedModule: T | undefined; for (const initialDir of searchPaths) { const resolvedPath = normalizeSlashes(host.resolvePath(combinePaths(initialDir, "node_modules"))); log(`Loading ${pluginConfigEntry.name} from ${initialDir} (resolved to ${resolvedPath})`); const result = host.require(resolvedPath, pluginConfigEntry.name); // TODO: GH#18217 if (!result.error) { resolvedModule = result.module as T; break; } const err = result.error.stack || result.error.message || JSON.stringify(result.error); (errorLogs ??= []).push(`Failed to load module '${pluginConfigEntry.name}' from ${resolvedPath}: ${err}`); } return { pluginConfigEntry, resolvedModule, errorLogs }; } /** @internal */ public static async importServicePluginAsync( pluginConfigEntry: PluginImport, searchPaths: string[], host: ServerHost, log: (message: string) => void, ): Promise> { Debug.assertIsDefined(host.importPlugin); let errorLogs: string[] | undefined; let resolvedModule: T | undefined; for (const initialDir of searchPaths) { const resolvedPath = combinePaths(initialDir, "node_modules"); log(`Dynamically importing ${pluginConfigEntry.name} from ${initialDir} (resolved to ${resolvedPath})`); let result: ModuleImportResult; try { result = await host.importPlugin(resolvedPath, pluginConfigEntry.name); } catch (e) { result = { module: undefined, error: e }; } if (!result.error) { resolvedModule = result.module as T; break; } const err = result.error.stack || result.error.message || JSON.stringify(result.error); (errorLogs ??= []).push(`Failed to dynamically import module '${pluginConfigEntry.name}' from ${resolvedPath}: ${err}`); } return { pluginConfigEntry, resolvedModule, errorLogs }; } /** @internal */ readonly currentDirectory: string; /** @internal */ readonly projectName: string; /** @internal */ public directoryStructureHost: DirectoryStructureHost; /** @internal */ public readonly getCanonicalFileName: GetCanonicalFileName; private exportMapCache: ExportInfoMap | undefined; private changedFilesForExportMapCache: Set | undefined; private moduleSpecifierCache = createModuleSpecifierCache(this); private symlinks: SymlinkCache | undefined; /** @internal */ autoImportProviderHost: AutoImportProviderProject | false | undefined; /** @internal */ protected typeAcquisition: TypeAcquisition | undefined; /** @internal */ createHash: ((data: string) => string) | undefined = maybeBind(this.projectService.host, this.projectService.host.createHash); /** @internal*/ preferNonRecursiveWatch: boolean | undefined; readonly jsDocParsingMode: JSDocParsingMode | undefined; /** @internal */ constructor( projectName: string, readonly projectKind: ProjectKind, readonly projectService: ProjectService, hasExplicitListOfFiles: boolean, lastFileExceededProgramSize: string | undefined, private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, protected watchOptions: WatchOptions | undefined, directoryStructureHost: DirectoryStructureHost, currentDirectory: string, ) { projectService.logger.info(`Creating ${ProjectKind[projectKind]}Project: ${projectName}, currentDirectory: ${currentDirectory}`); this.projectName = projectName; this.directoryStructureHost = directoryStructureHost; this.currentDirectory = this.projectService.getNormalizedAbsolutePath(currentDirectory); this.getCanonicalFileName = this.projectService.toCanonicalFileName; this.jsDocParsingMode = this.projectService.jsDocParsingMode; 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 || getAllowJSCompilerOption(this.compilerOptions) || this.projectService.hasDeferredExtension()) { // If files are listed explicitly or allowJs is specified, allow all extensions this.compilerOptions.allowNonTsExtensions = true; } switch (projectService.serverMode) { case LanguageServiceMode.Semantic: this.languageServiceEnabled = true; break; case LanguageServiceMode.PartialSemantic: this.languageServiceEnabled = true; this.compilerOptions.noResolve = true; this.compilerOptions.types = []; break; case LanguageServiceMode.Syntactic: this.languageServiceEnabled = false; this.compilerOptions.noResolve = true; this.compilerOptions.types = []; break; default: Debug.assertNever(projectService.serverMode); } 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); } this.realpath = maybeBind(host, host.realpath); this.preferNonRecursiveWatch = this.projectService.canUseWatchEvents || host.preferNonRecursiveWatch; // Use the current directory as resolution root only if the project created using current directory string this.resolutionCache = createResolutionCache( this, this.currentDirectory, /*logChangesWhenResolvingModule*/ true, ); this.languageService = createLanguageService( this, this.projectService.documentRegistry, this.projectService.serverMode, ); if (lastFileExceededProgramSize) { this.disableLanguageService(lastFileExceededProgramSize); } this.markAsDirty(); if (!isBackgroundProject(this)) { this.projectService.pendingEnsureProjectForOpenFiles = true; } this.projectService.onProjectCreation(this); } isKnownTypesPackageName(name: string): boolean { return this.projectService.typingsInstaller.isKnownTypesPackageName(name); } installPackage(options: InstallPackageOptions): Promise { return this.projectService.typingsInstaller.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) }); } /** @internal */ getGlobalTypingsCacheLocation(): string | undefined { return this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined; } /** @internal */ getSymlinkCache(): SymlinkCache { if (!this.symlinks) { this.symlinks = createSymlinkCache(this.getCurrentDirectory(), this.getCanonicalFileName); } if (this.program && !this.symlinks.hasProcessedResolutions()) { this.symlinks.setSymlinksFromResolutions( this.program.forEachResolvedModule, this.program.forEachResolvedTypeReferenceDirective, this.program.getAutomaticTypeDirectiveResolutions(), ); } return this.symlinks; } // Method of LanguageServiceHost getCompilationSettings(): CompilerOptions { return this.compilerOptions; } // Method to support public API getCompilerOptions(): CompilerOptions { return this.getCompilationSettings(); } getNewLine(): string { return this.projectService.host.newLine; } getProjectVersion(): string { return this.projectStateVersion.toString(); } getProjectReferences(): readonly ProjectReference[] | undefined { return undefined; } getScriptFileNames(): string[] { if (!this.rootFilesMap.size) { 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, /*deferredDeleteOk*/ false, ); 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 existingValue.info = scriptInfo; } scriptInfo.attachToProject(this); } return scriptInfo; } getScriptKind(fileName: string): ScriptKind { const info = this.projectService.getScriptInfoForPath(this.toPath(fileName)); return (info && info.scriptKind)!; // TODO: GH#18217 } getScriptVersion(filename: string): string { // Don't attach to the project if version is asked const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient( filename, this.currentDirectory, this.directoryStructureHost, /*deferredDeleteOk*/ false, ); 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(): string { const nodeModuleBinDir = getDirectoryPath(normalizePath(this.projectService.getExecutingFilePath())); return combinePaths(nodeModuleBinDir, getDefaultLibFileName(this.compilerOptions)); } useCaseSensitiveFileNames(): boolean { 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 { // 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.projectService.getScriptInfoForPath(path) || (!this.isWatchedMissingFile(path) && this.directoryStructureHost.fileExists(file)); } /** @internal */ resolveModuleNameLiterals(moduleLiterals: readonly StringLiteralLike[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions, containingSourceFile: SourceFile, reusedNames: readonly StringLiteralLike[] | undefined): readonly ResolvedModuleWithFailedLookupLocations[] { return this.resolutionCache.resolveModuleNameLiterals(moduleLiterals, containingFile, redirectedReference, options, containingSourceFile, reusedNames); } /** @internal */ getModuleResolutionCache(): ModuleResolutionCache | undefined { return this.resolutionCache.getModuleResolutionCache(); } /** @internal */ resolveTypeReferenceDirectiveReferences(typeDirectiveReferences: readonly T[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions, containingSourceFile: SourceFile | undefined, reusedNames: readonly T[] | undefined): readonly ResolvedTypeReferenceDirectiveWithFailedLookupLocations[] { return this.resolutionCache.resolveTypeReferenceDirectiveReferences( typeDirectiveReferences, containingFile, redirectedReference, options, containingSourceFile, reusedNames, ); } /** @internal */ resolveLibrary(libraryName: string, resolveFrom: string, options: CompilerOptions, libFileName: string): ResolvedModuleWithFailedLookupLocations { return this.resolutionCache.resolveLibrary(libraryName, resolveFrom, options, libFileName); } 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): Path { return toPath(fileName, this.currentDirectory, this.projectService.toCanonicalFileName); } /** @internal */ watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { return this.projectService.watchFactory.watchDirectory( directory, cb, flags, this.projectService.getWatchOptions(this), WatchType.FailedLookupLocations, this, ); } /** @internal */ watchAffectingFileLocation(file: string, cb: FileWatcherCallback): FileWatcher { return this.projectService.watchFactory.watchFile( file, cb, PollingInterval.High, this.projectService.getWatchOptions(this), WatchType.AffectingFileLocation, this, ); } /** @internal */ clearInvalidateResolutionOfFailedLookupTimer(): boolean { return this.projectService.throttledOperations.cancel(`${this.getProjectName()}FailedLookupInvalidation`); } /** @internal */ scheduleInvalidateResolutionsOfFailedLookupLocations(): void { this.projectService.throttledOperations.schedule(`${this.getProjectName()}FailedLookupInvalidation`, /*delay*/ 1000, () => { if (this.resolutionCache.invalidateResolutionsOfFailedLookupLocations()) { this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } }); } /** @internal */ invalidateResolutionsOfFailedLookupLocations(): void { if ( this.clearInvalidateResolutionOfFailedLookupTimer() && this.resolutionCache.invalidateResolutionsOfFailedLookupLocations() ) { this.markAsDirty(); this.projectService.delayEnsureProjectForOpenFiles(); } } /** @internal */ onInvalidatedResolution(): void { this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } /** @internal */ watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { return this.projectService.watchFactory.watchDirectory( directory, cb, flags, this.projectService.getWatchOptions(this), WatchType.TypeRoots, this, ); } /** @internal */ hasChangedAutomaticTypeDirectiveNames(): boolean { return this.resolutionCache.hasChangedAutomaticTypeDirectiveNames(); } /** @internal */ onChangedAutomaticTypeDirectiveNames(): void { this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } /** @internal */ globalCacheResolutionModuleName: typeof JsTyping.nonRelativeModuleNameForTypingCache = JsTyping.nonRelativeModuleNameForTypingCache; /** @internal */ fileIsOpen(filePath: Path): boolean { return this.projectService.openFiles.has(filePath); } /** @internal */ writeLog(s: string): void { this.projectService.logger.info(s); } log(s: string): void { this.writeLog(s); } error(s: string): void { 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 filter(this.projectErrors, diagnostic => !diagnostic.file) || emptyArray; } /** * Get all the project errors */ getAllProjectErrors(): readonly Diagnostic[] { return this.projectErrors || emptyArray; } setProjectErrors(projectErrors: Diagnostic[] | undefined): void { this.projectErrors = projectErrors; } getLanguageService(ensureSynchronized = true): LanguageService { if (ensureSynchronized) { updateProjectIfDirty(this); } return this.languageService; } /** @internal */ getSourceMapper(): SourceMapper { return this.getLanguageService().getSourceMapper(); } /** @internal */ clearSourceMapperCache(): void { this.languageService.clearSourceMapperCache(); } /** @internal */ getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { return this.projectService.getDocumentPositionMapper(this, generatedFileName, sourceFileName); } /** @internal */ getSourceFileLike(fileName: string): SourceFileLike | undefined { return this.projectService.getSourceFileLike(fileName, this); } /** @internal */ shouldEmitFile(scriptInfo: ScriptInfo | undefined): boolean | 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.builderState, /*disableUseFileVersionAsSignature*/ true); return mapDefined( BuilderState.getFilesAffectedBy( this.builderState, this.program!, scriptInfo.path, this.cancellationToken, this.projectService.host, ), 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): EmitResult { if (!this.languageServiceEnabled || !this.shouldEmitFile(scriptInfo)) { return { emitSkipped: true, diagnostics: emptyArray }; } const { emitSkipped, diagnostics, outputFiles } = this.getLanguageService().getEmitOutput(scriptInfo.fileName); if (!emitSkipped) { for (const outputFile of outputFiles) { const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, this.currentDirectory); writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); } // Update the signature if (this.builderState && getEmitDeclarations(this.compilerOptions)) { const dtsFiles = outputFiles.filter(f => isDeclarationFileName(f.name)); if (dtsFiles.length === 1) { const sourceFile = this.program!.getSourceFile(scriptInfo.fileName)!; const signature = this.projectService.host.createHash ? this.projectService.host.createHash(dtsFiles[0].text) : generateDjb2Hash(dtsFiles[0].text); BuilderState.updateSignatureOfFile(this.builderState, signature, sourceFile.resolvedPath); } } } return { emitSkipped, diagnostics }; } enableLanguageService(): void { if (this.languageServiceEnabled || this.projectService.serverMode === LanguageServiceMode.Syntactic) { return; } this.languageServiceEnabled = true; this.lastFileExceededProgramSize = undefined; this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ true); } /** @internal */ cleanupProgram(): void { if (this.program) { // Root files are always attached to the project irrespective of program for (const f of this.program.getSourceFiles()) { this.detachScriptInfoIfNotRoot(f.fileName); } this.program.forEachResolvedProjectReference(ref => this.detachScriptInfoFromProject(ref.sourceFile.fileName)); this.program = undefined; } } disableLanguageService(lastFileExceededProgramSize?: string): void { if (!this.languageServiceEnabled) { return; } Debug.assert(this.projectService.serverMode !== LanguageServiceMode.Syntactic); this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; this.cleanupProgram(); this.lastFileExceededProgramSize = lastFileExceededProgramSize; this.builderState = undefined; if (this.autoImportProviderHost) { this.autoImportProviderHost.close(); } this.autoImportProviderHost = undefined; this.resolutionCache.closeTypeRootsWatch(); this.clearGeneratedFileWatch(); this.projectService.verifyDocumentRegistry(); this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false); } getProjectName(): string { return this.projectName; } protected removeLocalTypingsFromTypeAcquisition(newTypeAcquisition: TypeAcquisition): TypeAcquisition { if (!newTypeAcquisition.enable || !newTypeAcquisition.include) { // Nothing to filter out, so just return as-is return newTypeAcquisition; } return { ...newTypeAcquisition, include: this.removeExistingTypings(newTypeAcquisition.include) }; } getExternalFiles(updateLevel?: ProgramUpdateLevel): SortedReadonlyArray { return toSorted(flatMap(this.plugins, plugin => { if (typeof plugin.module.getExternalFiles !== "function") return; try { return plugin.module.getExternalFiles(this, updateLevel || ProgramUpdateLevel.Update); } 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): SourceFile | undefined { 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(): void { if (this.typingsCache) this.projectService.typingsInstaller.onProjectClosed(this); this.typingsCache = undefined; this.closeWatchingTypingLocations(); // 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 this.cleanupProgram(); // Release external files forEach(this.externalFiles, externalFile => this.detachScriptInfoIfNotRoot(externalFile)); // Always remove root files from the project this.rootFilesMap.forEach(root => root.info?.detachFromProject(this)); this.projectService.pendingEnsureProjectForOpenFiles = true; this.rootFilesMap = undefined!; this.externalFiles = undefined; this.program = undefined; this.builderState = undefined; this.resolutionCache.clear(); this.resolutionCache = undefined!; this.cachedUnresolvedImportsPerFile = undefined!; this.packageJsonWatches?.forEach(watcher => { watcher.projects.delete(this); watcher.close(); }); this.packageJsonWatches = undefined; this.moduleSpecifierCache.clear(); this.moduleSpecifierCache = undefined!; this.directoryStructureHost = undefined!; this.exportMapCache = undefined; this.projectErrors = undefined; this.plugins.length = 0; // Clean up file watchers waiting for missing files if (this.missingFilesMap) { clearMap(this.missingFilesMap, closeFileWatcher); this.missingFilesMap = undefined; } this.clearGeneratedFileWatch(); this.clearInvalidateResolutionOfFailedLookupTimer(); if (this.autoImportProviderHost) { this.autoImportProviderHost.close(); } this.autoImportProviderHost = undefined; if (this.noDtsResolutionProject) { this.noDtsResolutionProject.close(); } this.noDtsResolutionProject = undefined; // 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(): boolean { return this.rootFilesMap === undefined; } hasRoots(): boolean { return !!this.rootFilesMap?.size; } /** @internal */ isOrphan() { return false; } getRootFiles(): NormalizedPath[] { return this.rootFilesMap && arrayFrom(ts.mapDefinedIterator(this.rootFilesMap.values(), value => value.info?.fileName)); } /** @internal */ getRootFilesMap(): Map { return this.rootFilesMap; } getRootScriptInfos(): ScriptInfo[] { return arrayFrom(ts.mapDefinedIterator(this.rootFilesMap.values(), value => value.info)); } getScriptInfos(): ScriptInfo[] { if (!this.languageServiceEnabled) { // if language service is not enabled - return just root files return this.getRootScriptInfos(); } 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): NormalizedPath[] { 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): protocol.FileWithProjectReferenceRedirectInfo[] { return this.getFileNames().map((fileName): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName, isSourceOfProjectReferenceRedirect: includeProjectReferenceRedirectInfo && this.isSourceOfProjectReferenceRedirect(fileName), })); } hasConfigFile(configFilePath: NormalizedPath): boolean { 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): boolean { return this.rootFilesMap?.get(info.path)?.info === info; } // add a root file to project addRoot(info: ScriptInfo, fileName?: NormalizedPath): void { Debug.assert(!this.isRoot(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): void { const path = this.projectService.toPath(fileName); this.rootFilesMap.set(path, { fileName }); this.markAsDirty(); } removeFile(info: ScriptInfo, fileExists: boolean, detachFromProject: boolean): void { 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): void { (this.updatedFileNames || (this.updatedFileNames = new Set())).add(fileName); } /** @internal */ markFileAsDirty(changedFile: Path): void { this.markAsDirty(); if (this.exportMapCache && !this.exportMapCache.isEmpty()) { (this.changedFilesForExportMapCache ||= new Set()).add(changedFile); } } /** @internal */ markAsDirty(): void { if (!this.dirty) { this.projectStateVersion++; this.dirty = true; } } /** @internal */ markAutoImportProviderAsDirty(): void { if (!this.autoImportProviderHost) this.autoImportProviderHost = undefined; this.autoImportProviderHost?.markAsDirty(); } /** @internal */ onAutoImportProviderSettingsChanged(): void { this.markAutoImportProviderAsDirty(); } /** @internal */ onPackageJsonChange(): void { this.moduleSpecifierCache.clear(); this.markAutoImportProviderAsDirty(); } /** @internal */ onFileAddedOrRemoved(isSymlink: boolean | undefined): void { this.hasAddedorRemovedFiles = true; if (isSymlink) { this.hasAddedOrRemovedSymlinks = true; } } /** @internal */ onDiscoveredSymlink(): void { this.hasAddedOrRemovedSymlinks = true; } /** @internal */ onReleaseOldSourceFile( oldSourceFile: SourceFile, _oldOptions: CompilerOptions, hasSourceFileByPath: boolean, newSourceFileByResolvedPath: SourceFile | undefined, ): void { if ( !newSourceFileByResolvedPath || (oldSourceFile.resolvedPath === oldSourceFile.path && newSourceFileByResolvedPath.resolvedPath !== oldSourceFile.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(oldSourceFile.fileName, hasSourceFileByPath); } } /** @internal */ updateFromProjectInProgress = false; /** @internal */ updateFromProject(): void { updateProjectIfDirty(this); } /** * 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 { tracing?.push(tracing.Phase.Session, "updateGraph", { name: this.projectName, kind: ProjectKind[this.projectKind] }); this.resolutionCache.startRecordingFilesWithChangedResolutions(); const hasNewProgram = this.updateGraphWorker(); const hasAddedorRemovedFiles = this.hasAddedorRemovedFiles; this.hasAddedorRemovedFiles = false; this.hasAddedOrRemovedSymlinks = 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 && this.projectService.serverMode === LanguageServiceMode.Semantic && !this.isOrphan()) { // 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.enqueueInstallTypingsForProject(hasAddedorRemovedFiles); } else { this.lastCachedUnresolvedImportsList = undefined; } const isFirstProgramLoad = this.projectProgramVersion === 0 && hasNewProgram; if (hasNewProgram) { this.projectProgramVersion++; } if (hasAddedorRemovedFiles) { this.markAutoImportProviderAsDirty(); } if (isFirstProgramLoad) { // Preload auto import provider so it's not created during completions request this.getPackageJsonAutoImportProvider(); } tracing?.pop(); return !hasNewProgram; } /** @internal */ enqueueInstallTypingsForProject(forceRefresh: boolean): void { const typeAcquisition = this.getTypeAcquisition(); if (!typeAcquisition || !typeAcquisition.enable || this.projectService.typingsInstaller === nullTypingsInstaller) { return; } const entry = this.typingsCache; if ( forceRefresh || !entry || typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) || compilerOptionsChanged(this.getCompilationSettings(), entry.compilerOptions) || unresolvedImportsChanged(this.lastCachedUnresolvedImportsList, entry.unresolvedImports) ) { // Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options. // instead it acts as a placeholder to prevent issuing multiple requests this.typingsCache = { compilerOptions: this.getCompilationSettings(), typeAcquisition, unresolvedImports: this.lastCachedUnresolvedImportsList, }; // something has been changed, issue a request to update typings this.projectService.typingsInstaller.enqueueInstallTypingsRequest(this, typeAcquisition, this.lastCachedUnresolvedImportsList); } } /** @internal */ updateTypingFiles(compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, newTypings: string[]): void { this.typingsCache = { compilerOptions, typeAcquisition, unresolvedImports, }; const typingFiles = !typeAcquisition || !typeAcquisition.enable ? emptyArray : toSorted(newTypings); if (enumerateInsertsAndDeletes(typingFiles, this.typingFiles, getStringComparer(!this.useCaseSensitiveFileNames()), /*inserted*/ noop, removed => this.detachScriptInfoFromProject(removed))) { // If typing files changed, then only schedule project update this.typingFiles = typingFiles; // Invalidate files with unresolved imports this.resolutionCache.setFilesWithInvalidatedNonRelativeUnresolvedImports(this.cachedUnresolvedImportsPerFile); this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } } private closeWatchingTypingLocations() { if (this.typingWatchers) clearMap(this.typingWatchers, closeFileWatcher); this.typingWatchers = undefined; } private onTypingInstallerWatchInvoke() { this.typingWatchers!.isInvoked = true; this.projectService.updateTypingsForProject({ projectName: this.getProjectName(), kind: ActionInvalidate }); } /** @internal */ watchTypingLocations(files: readonly string[] | undefined): void { // Skip watching typing locations for inferred project whose currentDirectory is not watchable or // is same as server's current directory if ( this.currentDirectory === this.projectService.currentDirectory || !canWatchDirectoryOrFilePath(this.toPath(this.currentDirectory)) ) { return; } if (!files) { this.typingWatchers!.isInvoked = false; return; } if (!files.length) { // shut down existing watchers this.closeWatchingTypingLocations(); return; } const toRemove = new Map(this.typingWatchers); if (!this.typingWatchers) this.typingWatchers = new Map(); // handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings this.typingWatchers.isInvoked = false; const createProjectWatcher = (path: string, typingsWatcherType: TypingWatcherType) => { const canonicalPath = this.toPath(path); toRemove.delete(canonicalPath); if (!this.typingWatchers!.has(canonicalPath)) { const watchType = typingsWatcherType === TypingWatcherType.FileWatcher ? WatchType.TypingInstallerLocationFile : WatchType.TypingInstallerLocationDirectory; this.typingWatchers!.set( canonicalPath, canWatchDirectoryOrFilePath(canonicalPath) ? typingsWatcherType === TypingWatcherType.FileWatcher ? this.projectService.watchFactory.watchFile( path, () => !this.typingWatchers!.isInvoked ? this.onTypingInstallerWatchInvoke() : this.writeLog(`TypingWatchers already invoked`), PollingInterval.High, this.projectService.getWatchOptions(this), watchType, this, ) : this.projectService.watchFactory.watchDirectory( path, f => { if (this.typingWatchers!.isInvoked) return this.writeLog(`TypingWatchers already invoked`); if (!fileExtensionIs(f, Extension.Json)) return this.writeLog(`Ignoring files that are not *.json`); if (comparePaths(f, combinePaths(this.projectService.typingsInstaller.globalTypingsCacheLocation!, "package.json"), !this.useCaseSensitiveFileNames())) return this.writeLog(`Ignoring package.json change at global typings location`); this.onTypingInstallerWatchInvoke(); }, WatchDirectoryFlags.Recursive, this.projectService.getWatchOptions(this), watchType, this, ) : (this.writeLog(`Skipping watcher creation at ${path}:: ${getDetailWatchInfo(watchType, this)}`), noopFileWatcher), ); } }; // Create watches from list of files for (const file of files) { const basename = getBaseFileName(file); if (basename === "package.json" || basename === "bower.json") { // package.json or bower.json exists, watch the file to detect changes and update typings createProjectWatcher(file, TypingWatcherType.FileWatcher); continue; } // path in projectRoot, watch project root if (containsPath(this.currentDirectory, file, this.currentDirectory, !this.useCaseSensitiveFileNames())) { const subDirectory = file.indexOf(directorySeparator, this.currentDirectory.length + 1); if (subDirectory !== -1) { // Watch subDirectory createProjectWatcher(file.substr(0, subDirectory), TypingWatcherType.DirectoryWatcher); } else { // Watch the directory itself createProjectWatcher(file, TypingWatcherType.DirectoryWatcher); } continue; } // path in global cache, watch global cache if (containsPath(this.projectService.typingsInstaller.globalTypingsCacheLocation!, file, this.currentDirectory, !this.useCaseSensitiveFileNames())) { createProjectWatcher(this.projectService.typingsInstaller.globalTypingsCacheLocation!, TypingWatcherType.DirectoryWatcher); continue; } // watch node_modules or bower_components createProjectWatcher(file, TypingWatcherType.DirectoryWatcher); } // Remove unused watches toRemove.forEach((watch, path) => { watch.close(); this.typingWatchers!.delete(path); }); } /** @internal */ skipWatchingFailedLookups(path: Path): boolean | undefined { const info = this.projectService.getScriptInfoForPath(path); return info?.isDynamic; } /** @internal */ skipWatchingTypeRoots(): boolean | undefined { // Skip watching inferrd project where current directory is lib location return isInferredProject(this) && this.currentDirectory === this.projectService.currentDirectory; } /** @internal */ getCurrentProgram(): Program | undefined { return this.program; } protected removeExistingTypings(include: string[]): string[] { if (!include.length) return include; const existing = getAutomaticTypeDirectiveNames(this.getCompilerOptions(), this); return filter(include, i => !existing.includes(i)); } private updateGraphWorker() { const oldProgram = this.languageService.getCurrentProgram(); Debug.assert(oldProgram === this.program); Debug.assert(!this.isClosed(), "Called update graph worker of closed project"); this.writeLog(`Starting updateGraphWorker: Project: ${this.getProjectName()}`); const start = timestamp(); const { hasInvalidatedResolutions, hasInvalidatedLibResolutions } = this.resolutionCache.createHasInvalidatedResolutions(returnFalse, returnFalse); this.hasInvalidatedResolutions = hasInvalidatedResolutions; this.hasInvalidatedLibResolutions = hasInvalidatedLibResolutions; this.resolutionCache.startCachingPerDirectoryResolution(); this.dirty = false; this.updateFromProjectInProgress = true; this.program = this.languageService.getProgram(); // TODO: GH#18217 this.updateFromProjectInProgress = false; tracing?.push(tracing.Phase.Session, "finishCachingPerDirectoryResolution"); this.resolutionCache.finishCachingPerDirectoryResolution(this.program, oldProgram); tracing?.pop(); 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. let hasNewProgram = false; if (this.program && (!oldProgram || (this.program !== oldProgram && this.program.structureIsReused !== StructureIsReused.Completely))) { hasNewProgram = true; // Update roots this.rootFilesMap.forEach((value, path) => { const file = this.program!.getSourceFileByPath(path); const info = value.info; if (!file || value.info?.path === file.resolvedPath) return; value.info = this.projectService.getScriptInfo(file.fileName)!; Debug.assert(value.info.isAttached(this)); info?.detachFromProject(this); }); // Update the missing file paths watcher updateMissingFilePathsWatch( this.program, this.missingFilesMap || (this.missingFilesMap = new Map()), // Watch the missing files (missingFilePath, missingFileName) => this.addMissingFileWatcher(missingFilePath, missingFileName), ); if (this.generatedFilesMap) { const outPath = this.compilerOptions.outFile; 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); if ( !sourceFile || sourceFile.resolvedPath !== source || !this.isValidGeneratedFileWatcher( getDeclarationEmitOutputFilePathWorker(sourceFile.fileName, this.compilerOptions, this.program!), 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.projectService.serverMode === LanguageServiceMode.Semantic) { this.resolutionCache.updateTypeRootsWatch(); } } this.projectService.verifyProgram(this); if (this.exportMapCache && !this.exportMapCache.isEmpty()) { this.exportMapCache.releaseSymbols(); if (this.hasAddedorRemovedFiles || oldProgram && !this.program!.structureIsReused) { this.exportMapCache.clear(); } else if (this.changedFilesForExportMapCache && oldProgram && this.program) { forEachKey(this.changedFilesForExportMapCache, fileName => { const oldSourceFile = oldProgram.getSourceFileByPath(fileName); const sourceFile = this.program!.getSourceFileByPath(fileName); if (!oldSourceFile || !sourceFile) { this.exportMapCache!.clear(); return true; } return this.exportMapCache!.onFileChanged(oldSourceFile, sourceFile, !!this.getTypeAcquisition().enable); }); } } if (this.changedFilesForExportMapCache) { this.changedFilesForExportMapCache.clear(); } if (this.hasAddedOrRemovedSymlinks || this.program && !this.program.structureIsReused && this.getCompilerOptions().preserveSymlinks) { // With --preserveSymlinks, we may not determine that a file is a symlink, so we never set `hasAddedOrRemovedSymlinks` this.symlinks = undefined; this.moduleSpecifierCache.clear(); } 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, /*deferredDeleteOk*/ false, ); scriptInfo?.attachToProject(this); }, removed => this.detachScriptInfoFromProject(removed), ); const elapsed = timestamp() - start; this.sendPerformanceEvent("UpdateGraph", elapsed); this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} projectStateVersion: ${this.projectStateVersion} projectProgramVersion: ${this.projectProgramVersion} structureChanged: ${hasNewProgram}${this.program ? ` structureIsReused:: ${(ts as any).StructureIsReused[this.program.structureIsReused]}` : ""} Elapsed: ${elapsed}ms`); if (this.projectService.logger.isTestLogger) { if (this.program !== oldProgram) { this.print(/*writeProjectFileNames*/ true, this.hasAddedorRemovedFiles, /*writeFileVersionAndText*/ true); } else { this.writeLog(`Same program as before`); } } else if (this.hasAddedorRemovedFiles) { this.print(/*writeProjectFileNames*/ true, /*writeFileExplaination*/ true, /*writeFileVersionAndText*/ false); } else if (this.program !== oldProgram) { this.writeLog(`Different program with same set of files`); } // Verify the document registry count this.projectService.verifyDocumentRegistry(); return hasNewProgram; } /** @internal */ sendPerformanceEvent(kind: PerformanceEvent["kind"], durationMs: number): void { this.projectService.sendPerformanceEvent(kind, durationMs); } 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, missingFileName: string): FileWatcher { if (isConfiguredProject(this)) { // If this file is referenced config file, we are already watching it, no need to watch again const configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(missingFilePath as string as NormalizedPath); if (configFileExistenceInfo?.config?.projects.has(this.canonicalConfigFilePath)) return noopFileWatcher; } const fileWatcher = this.projectService.watchFactory.watchFile( getNormalizedAbsolutePath(missingFileName, this.currentDirectory), (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): void { if (this.compilerOptions.outFile) { // 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 = new Map(); } this.generatedFilesMap.set(path, this.createGeneratedFileWatcher(generatedFile)); } } private createGeneratedFileWatcher(generatedFile: string): GeneratedFileWatcher { return { generatedFilePath: this.toPath(generatedFile), watcher: this.projectService.watchFactory.watchFile( generatedFile, () => { this.clearSourceMapperCache(); 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): ScriptInfo | undefined { return this.projectService.getScriptInfo(uncheckedFileName); } filesToString(writeProjectFileNames: boolean): string { return this.filesToStringWorker(writeProjectFileNames, /*writeFileExplaination*/ true, /*writeFileVersionAndText*/ false); } private filesToStringWorker(writeProjectFileNames: boolean, writeFileExplaination: boolean, writeFileVersionAndText: boolean) { if (this.initialLoadPending) 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}${writeFileVersionAndText ? ` ${file.version} ${JSON.stringify(file.text)}` : ""}\n`; } if (writeFileExplaination) { strBuilder += "\n\n"; explainFiles(this.program, s => strBuilder += `\t${s}\n`); } } return strBuilder; } /** @internal */ print(writeProjectFileNames: boolean, writeFileExplaination: boolean, writeFileVersionAndText: boolean): void { this.writeLog(`Project '${this.projectName}' (${ProjectKind[this.projectKind]})`); this.writeLog(this.filesToStringWorker( writeProjectFileNames && this.projectService.logger.hasLevel(LogLevel.verbose), writeFileExplaination && this.projectService.logger.hasLevel(LogLevel.verbose), writeFileVersionAndText && this.projectService.logger.hasLevel(LogLevel.verbose), )); this.writeLog("-----------------------------------------------"); if (this.autoImportProviderHost) { this.autoImportProviderHost.print(/*writeProjectFileNames*/ false, /*writeFileExplaination*/ false, /*writeFileVersionAndText*/ false); } this.noDtsResolutionProject?.print(/*writeProjectFileNames*/ false, /*writeFileExplaination*/ false, /*writeFileVersionAndText*/ false); } setCompilerOptions(compilerOptions: CompilerOptions): void { if (compilerOptions) { compilerOptions.allowNonTsExtensions = true; const oldOptions = this.compilerOptions; this.compilerOptions = compilerOptions; this.setInternalCompilerOptionsForEmittingJsFiles(); this.noDtsResolutionProject?.setCompilerOptions(this.getCompilerOptionsForNoDtsResolutionProject()); if (changesAffectModuleResolution(oldOptions, compilerOptions)) { // reset cached unresolved imports if changes in compiler options affected module resolution this.cachedUnresolvedImportsPerFile.clear(); this.lastCachedUnresolvedImportsList = undefined; this.resolutionCache.onChangesAffectModuleResolution(); this.moduleSpecifierCache.clear(); } this.markAsDirty(); } } /** @internal */ setWatchOptions(watchOptions: WatchOptions | undefined): void { this.watchOptions = watchOptions; } /** @internal */ getWatchOptions(): WatchOptions | undefined { return this.watchOptions; } setTypeAcquisition(newTypeAcquisition: TypeAcquisition | undefined): void { if (newTypeAcquisition) { this.typeAcquisition = this.removeLocalTypingsFromTypeAcquisition(newTypeAcquisition); } } getTypeAcquisition(): TypeAcquisition { return this.typeAcquisition || {}; } /** @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.initialLoadPending) { 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.externalFiles?.map((f): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName: toNormalizedPath(f), isSourceOfProjectReferenceRedirect: false, })) || emptyArray; 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.externalFiles?.map((f): protocol.FileWithProjectReferenceRedirectInfo => ({ fileName: toNormalizedPath(f), isSourceOfProjectReferenceRedirect: false, })) || emptyArray; 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 { this.rootFilesMap.delete(info.path); } /** @internal */ isSourceOfProjectReferenceRedirect(fileName: string): boolean { return !!this.program && this.program.isSourceOfProjectReferenceRedirect(fileName); } /** @internal */ protected getGlobalPluginSearchPaths(): string[] { // Search any globally-specified probe paths, then our peer node_modules return [ ...this.projectService.pluginProbeLocations, // ../../.. to walk from X/node_modules/typescript/lib/tsserver.js to X/node_modules/ combinePaths(this.projectService.getExecutingFilePath(), "../../.."), ]; } protected enableGlobalPlugins(options: CompilerOptions): void { if (!this.projectService.globalPlugins.length) return; const host = this.projectService.host; if (!host.require && !host.importPlugin) { this.projectService.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded"); return; } // Enable global plugins with synthetic configuration entries const searchPaths = this.getGlobalPluginSearchPaths(); 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); } } protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[]): void { this.projectService.requestEnablePlugin(this, pluginConfigEntry, searchPaths); } /** @internal */ enableProxy(pluginModuleFactory: PluginModuleFactory, configEntry: PluginImport): void { 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, session: this.projectService.session, }; const pluginModule = pluginModuleFactory({ typescript: ts }); const newLS = pluginModule.create(info); for (const k of Object.keys(this.languageService)) { // eslint-disable-next-line local/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 succeeded`); 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): void { 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(): void { this.projectService.sendProjectsUpdatedInBackgroundEvent(); } /** @internal */ getPackageJsonsVisibleToFile(fileName: string, rootDir?: string): readonly ProjectPackageJsonInfo[] { if (this.projectService.serverMode !== LanguageServiceMode.Semantic) return emptyArray; return this.projectService.getPackageJsonsVisibleToFile(fileName, this, rootDir); } /** @internal */ getNearestAncestorDirectoryWithPackageJson(fileName: string): string | undefined { return this.projectService.getNearestAncestorDirectoryWithPackageJson(fileName, this); } /** @internal */ getPackageJsonsForAutoImport(rootDir?: string): readonly ProjectPackageJsonInfo[] { return this.getPackageJsonsVisibleToFile(combinePaths(this.currentDirectory, inferredTypesContainingFile), rootDir); } /** @internal */ getPackageJsonCache(): PackageJsonCache { return this.projectService.packageJsonCache; } /** @internal */ getCachedExportInfoMap(): ExportInfoMap { return this.exportMapCache ||= createCacheableExportInfoMap(this); } /** @internal */ clearCachedExportInfoMap(): void { this.exportMapCache?.clear(); } /** @internal */ getModuleSpecifierCache(): ModuleSpecifierCache { return this.moduleSpecifierCache; } /** @internal */ includePackageJsonAutoImports(): PackageJsonAutoImportPreference { if ( this.projectService.includePackageJsonAutoImports() === PackageJsonAutoImportPreference.Off || !this.languageServiceEnabled || isInsideNodeModules(this.currentDirectory) || !this.isDefaultProjectForOpenFiles() ) { return PackageJsonAutoImportPreference.Off; } return this.projectService.includePackageJsonAutoImports(); } /** @internal */ getHostForAutoImportProvider(): GetPackageJsonEntrypointsHost { if (this.program) { return { fileExists: this.program.fileExists, directoryExists: this.program.directoryExists, realpath: this.program.realpath || this.projectService.host.realpath?.bind(this.projectService.host), getCurrentDirectory: this.getCurrentDirectory.bind(this), readFile: this.projectService.host.readFile.bind(this.projectService.host), getDirectories: this.projectService.host.getDirectories.bind(this.projectService.host), trace: this.projectService.host.trace?.bind(this.projectService.host), useCaseSensitiveFileNames: this.program.useCaseSensitiveFileNames(), readDirectory: this.projectService.host.readDirectory.bind(this.projectService.host), }; } return this.projectService.host; } /** @internal */ getPackageJsonAutoImportProvider(): Program | undefined { if (this.autoImportProviderHost === false) { return undefined; } if (this.projectService.serverMode !== LanguageServiceMode.Semantic) { this.autoImportProviderHost = false; return undefined; } if (this.autoImportProviderHost) { updateProjectIfDirty(this.autoImportProviderHost); if (this.autoImportProviderHost.isEmpty()) { this.autoImportProviderHost.close(); this.autoImportProviderHost = undefined; return undefined; } return this.autoImportProviderHost.getCurrentProgram(); } const dependencySelection = this.includePackageJsonAutoImports(); if (dependencySelection) { tracing?.push(tracing.Phase.Session, "getPackageJsonAutoImportProvider"); const start = timestamp(); this.autoImportProviderHost = AutoImportProviderProject.create( dependencySelection, this, this.getHostForAutoImportProvider(), ) ?? false; if (this.autoImportProviderHost) { updateProjectIfDirty(this.autoImportProviderHost); this.sendPerformanceEvent("CreatePackageJsonAutoImportProvider", timestamp() - start); tracing?.pop(); return this.autoImportProviderHost.getCurrentProgram(); } tracing?.pop(); } } private isDefaultProjectForOpenFiles(): boolean { return !!forEachEntry( this.projectService.openFiles, (_projectRootPath, path) => this.projectService.tryGetDefaultProjectForFile(this.projectService.getScriptInfoForPath(path)!) === this, ); } /** @internal */ watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher { return this.projectService.watchPackageJsonsInNodeModules(directoryPath, this); } /** @internal */ getIncompleteCompletionsCache(): IncompleteCompletionsCache { return this.projectService.getIncompleteCompletionsCache(); } /** @internal */ getNoDtsResolutionProject(rootFile: NormalizedPath): AuxiliaryProject { Debug.assert(this.projectService.serverMode === LanguageServiceMode.Semantic); this.noDtsResolutionProject ??= new AuxiliaryProject(this); if (this.noDtsResolutionProject.rootFile !== rootFile) { this.projectService.setFileNamesOfAutoImportProviderOrAuxillaryProject( this.noDtsResolutionProject, [rootFile], ); this.noDtsResolutionProject.rootFile = rootFile; } return this.noDtsResolutionProject; } /** @internal */ runWithTemporaryFileUpdate(rootFile: string, updatedText: string, cb: (updatedProgram: Program, originalProgram: Program | undefined, updatedFile: SourceFile) => void): void { const originalProgram = this.program; const rootSourceFile = Debug.checkDefined(this.program?.getSourceFile(rootFile), "Expected file to be part of program"); const originalText = Debug.checkDefined(rootSourceFile.getFullText()); this.getScriptInfo(rootFile)?.editContent(0, originalText.length, updatedText); this.updateGraph(); try { cb(this.program!, originalProgram, (this.program?.getSourceFile(rootFile))!); } finally { this.getScriptInfo(rootFile)?.editContent(0, updatedText.length, originalText); } } /** @internal */ getCompilerOptionsForNoDtsResolutionProject(): CompilerOptions { return { ...this.getCompilerOptions(), noDtsResolution: true, allowJs: true, maxNodeModuleJsDepth: 3, diagnostics: false, skipLibCheck: true, sourceMap: false, types: ts.emptyArray, lib: ts.emptyArray, noLib: true, }; } } function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: Map): SortedReadonlyArray { const sourceFiles = program.getSourceFiles(); tracing?.push(tracing.Phase.Session, "getUnresolvedImports", { count: sourceFiles.length }); const ambientModules = program.getTypeChecker().getAmbientModules().map(mod => stripQuotes(mod.getName())); const result = sortAndDeduplicate(flatMap(sourceFiles, sourceFile => extractUnresolvedImportsFromSourceFile( program, sourceFile, ambientModules, cachedUnresolvedImportsPerFile, ))); tracing?.pop(); return result; } function extractUnresolvedImportsFromSourceFile( program: Program, file: SourceFile, ambientModules: readonly string[], cachedUnresolvedImportsPerFile: Map, ): readonly string[] { return getOrUpdate(cachedUnresolvedImportsPerFile, file.path, () => { let unresolvedImports: string[] | undefined; program.forEachResolvedModule(({ 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); } }, file); 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 _isJsInferredProject = false; toggleJsInferredProject(isJsInferredProject: boolean): void { if (isJsInferredProject !== this._isJsInferredProject) { this._isJsInferredProject = isJsInferredProject; this.setCompilerOptions(); } } override setCompilerOptions(options?: CompilerOptions): void { // 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; /** * stored only if their is no projectRootPath and this isnt single inferred project * * @internal */ readonly canonicalCurrentDirectory: string | undefined; /** @internal */ constructor( projectService: ProjectService, compilerOptions: CompilerOptions, watchOptions: WatchOptions | undefined, projectRootPath: NormalizedPath | undefined, currentDirectory: string, typeAcquisition: TypeAcquisition | undefined, ) { super( projectService.newInferredProjectName(), ProjectKind.Inferred, projectService, /*hasExplicitListOfFiles*/ false, /*lastFileExceededProgramSize*/ undefined, compilerOptions, /*compileOnSaveEnabled*/ false, watchOptions, projectService.host, currentDirectory, ); this.typeAcquisition = typeAcquisition; this.projectRootPath = projectRootPath && projectService.toCanonicalFileName(projectRootPath); if (!projectRootPath && !projectService.useSingleInferredProject) { this.canonicalCurrentDirectory = projectService.toCanonicalFileName(this.currentDirectory); } this.enableGlobalPlugins(this.getCompilerOptions()); } override addRoot(info: ScriptInfo): void { Debug.assert(info.isScriptOpen()); this.projectService.startWatchingConfigFilesForInferredProjectRoot(info); if (!this._isJsInferredProject && info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } else if (this.isOrphan() && this._isJsInferredProject && !info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ false); } super.addRoot(info); } override removeRoot(info: ScriptInfo): void { this.projectService.stopWatchingConfigFilesForScriptInfo(info); super.removeRoot(info); // Delay toggling to isJsInferredProject = false till we actually need it again if (!this.isOrphan() && this._isJsInferredProject && info.isJavaScript()) { if (every(this.getRootScriptInfos(), rootInfo => !rootInfo.isJavaScript())) { this.toggleJsInferredProject(/*isJsInferredProject*/ false); } } } /** @internal */ override isOrphan(): boolean { return !this.hasRoots(); } isProjectWithSingleRoot(): boolean { // - 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; } override close(): void { forEach(this.getRootScriptInfos(), info => this.projectService.stopWatchingConfigFilesForScriptInfo(info)); super.close(); } override getTypeAcquisition(): TypeAcquisition { return this.typeAcquisition || { enable: allRootFilesAreJsOrDts(this), include: ts.emptyArray, exclude: ts.emptyArray, }; } } /** @internal */ export class AuxiliaryProject extends Project { /** @internal */ rootFile: NormalizedPath | undefined; constructor(hostProject: Project) { super( hostProject.projectService.newAuxiliaryProjectName(), ProjectKind.Auxiliary, hostProject.projectService, /*hasExplicitListOfFiles*/ false, /*lastFileExceededProgramSize*/ undefined, hostProject.getCompilerOptionsForNoDtsResolutionProject(), /*compileOnSaveEnabled*/ false, /*watchOptions*/ undefined, hostProject.projectService.host, hostProject.currentDirectory, ); } override isOrphan(): boolean { return true; } override scheduleInvalidateResolutionsOfFailedLookupLocations(): void { // Invalidation will happen on-demand as part of updateGraph return; } } export class AutoImportProviderProject extends Project { private static readonly maxDependencies = 10; /** @internal */ static getRootFileNames(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, host: GetPackageJsonEntrypointsHost, compilerOptions: CompilerOptions): string[] { if (!dependencySelection) { return ts.emptyArray; } const program = hostProject.getCurrentProgram(); if (!program) { return ts.emptyArray; } const start = timestamp(); let dependencyNames: Set | undefined; let rootNames: Set | undefined; const rootFileName = combinePaths(hostProject.currentDirectory, inferredTypesContainingFile); const packageJsons = hostProject.getPackageJsonsForAutoImport(combinePaths(hostProject.currentDirectory, rootFileName)); for (const packageJson of packageJsons) { packageJson.dependencies?.forEach((_, dependenyName) => addDependency(dependenyName)); packageJson.peerDependencies?.forEach((_, dependencyName) => addDependency(dependencyName)); } let dependenciesAdded = 0; if (dependencyNames) { const symlinkCache = hostProject.getSymlinkCache(); for (const name of arrayFrom(dependencyNames.keys())) { // Avoid creating a large project that would significantly slow down time to editor interactivity if (dependencySelection === PackageJsonAutoImportPreference.Auto && dependenciesAdded >= this.maxDependencies) { hostProject.log(`AutoImportProviderProject: attempted to add more than ${this.maxDependencies} dependencies. Aborting.`); return ts.emptyArray; } // 1. Try to load from the implementation package. For many dependencies, the // package.json will exist, but the package will not contain any typings, // so `entrypoints` will be undefined. In that case, or if the dependency // is missing altogether, we will move on to trying the @types package (2). const packageJson = resolvePackageNameToPackageJson( name, hostProject.currentDirectory, compilerOptions, host, program.getModuleResolutionCache(), ); if (packageJson) { const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache); if (entrypoints) { dependenciesAdded += addRootNames(entrypoints); continue; } } // 2. Try to load from the @types package in the tree and in the global // typings cache location, if enabled. const done = forEach([hostProject.currentDirectory, hostProject.getGlobalTypingsCacheLocation()], directory => { if (directory) { const typesPackageJson = resolvePackageNameToPackageJson( `@types/${name}`, directory, compilerOptions, host, program.getModuleResolutionCache(), ); if (typesPackageJson) { const entrypoints = getRootNamesFromPackageJson(typesPackageJson, program, symlinkCache); dependenciesAdded += addRootNames(entrypoints); return true; } } }); if (done) continue; // 3. If the @types package did not exist and the user has settings that // allow processing JS from node_modules, go back to the implementation // package and load the JS. if (packageJson && compilerOptions.allowJs && compilerOptions.maxNodeModuleJsDepth) { const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache, /*resolveJs*/ true); dependenciesAdded += addRootNames(entrypoints); } } } const references = program.getResolvedProjectReferences(); let referencesAddded = 0; if (references?.length && hostProject.projectService.getHostPreferences().includeCompletionsForModuleExports) { // Add direct referenced projects to rootFiles names references.forEach(ref => { if (ref?.commandLine.options.outFile) { referencesAddded += addRootNames(filterEntrypoints([ changeExtension(ref.commandLine.options.outFile, ".d.ts"), ])); } else if (ref) { const getCommonSourceDirectory = memoize(() => getCommonSourceDirectoryOfConfig( ref.commandLine, !hostProject.useCaseSensitiveFileNames(), ) ); referencesAddded += addRootNames(filterEntrypoints(mapDefined( ref.commandLine.fileNames, fileName => !isDeclarationFileName(fileName) && !fileExtensionIs(fileName, Extension.Json) && !program.getSourceFile(fileName) ? getOutputDeclarationFileName( fileName, ref.commandLine, !hostProject.useCaseSensitiveFileNames(), getCommonSourceDirectory, ) : undefined, ))); } }); } if (rootNames?.size) { hostProject.log(`AutoImportProviderProject: found ${rootNames.size} root files in ${dependenciesAdded} dependencies ${referencesAddded} referenced projects in ${timestamp() - start} ms`); } return rootNames ? arrayFrom(rootNames.values()) : ts.emptyArray; function addRootNames(entrypoints: readonly string[] | undefined) { if (!entrypoints?.length) return 0; rootNames ??= new Set(); entrypoints.forEach(entry => rootNames!.add(entry)); return 1; } function addDependency(dependency: string) { if (!startsWith(dependency, "@types/")) { (dependencyNames || (dependencyNames = new Set())).add(dependency); } } function getRootNamesFromPackageJson(packageJson: PackageJsonInfo, program: Program, symlinkCache: SymlinkCache, resolveJs?: boolean) { const entrypoints = getEntrypointsFromPackageJsonInfo( packageJson, compilerOptions, host, program.getModuleResolutionCache(), resolveJs, ); if (entrypoints) { const real = host.realpath?.(packageJson.packageDirectory); const realPath = real ? hostProject.toPath(real) : undefined; const isSymlink = realPath && realPath !== hostProject.toPath(packageJson.packageDirectory); if (isSymlink) { symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, { real: ensureTrailingDirectorySeparator(real!), realPath: ensureTrailingDirectorySeparator(realPath), }); } return filterEntrypoints(entrypoints, isSymlink ? entrypoint => entrypoint.replace(packageJson.packageDirectory, real!) : undefined); } } function filterEntrypoints(entrypoints: readonly string[] | undefined, symlinkName?: (entrypoint: string) => string) { return mapDefined(entrypoints, entrypoint => { const resolvedFileName = symlinkName ? symlinkName(entrypoint) : entrypoint; if (!program!.getSourceFile(resolvedFileName) && !(symlinkName && program!.getSourceFile(entrypoint))) { return resolvedFileName; } }); } } /** @internal */ static readonly compilerOptionsOverrides: CompilerOptions = { diagnostics: false, skipLibCheck: true, sourceMap: false, types: ts.emptyArray, lib: ts.emptyArray, noLib: true, }; /** @internal */ static create( dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, host: GetPackageJsonEntrypointsHost, ): AutoImportProviderProject | undefined { if (dependencySelection === PackageJsonAutoImportPreference.Off) { return undefined; } const compilerOptions = { ...hostProject.getCompilerOptions(), ...this.compilerOptionsOverrides, }; const rootNames = this.getRootFileNames(dependencySelection, hostProject, host, compilerOptions); if (!rootNames.length) { return undefined; } return new AutoImportProviderProject(hostProject, rootNames, compilerOptions); } private rootFileNames: string[] | undefined; /** @internal */ constructor( private hostProject: Project, initialRootNames: string[], compilerOptions: CompilerOptions, ) { super( hostProject.projectService.newAutoImportProviderProjectName(), ProjectKind.AutoImportProvider, hostProject.projectService, /*hasExplicitListOfFiles*/ false, /*lastFileExceededProgramSize*/ undefined, compilerOptions, /*compileOnSaveEnabled*/ false, hostProject.getWatchOptions(), hostProject.projectService.host, hostProject.currentDirectory, ); this.rootFileNames = initialRootNames; this.useSourceOfProjectReferenceRedirect = maybeBind(this.hostProject, this.hostProject.useSourceOfProjectReferenceRedirect); this.getParsedCommandLine = maybeBind(this.hostProject, this.hostProject.getParsedCommandLine); } /** @internal */ isEmpty(): boolean { return !some(this.rootFileNames); } /** @internal */ override isOrphan() { return true; } override updateGraph(): boolean { let rootFileNames = this.rootFileNames; if (!rootFileNames) { rootFileNames = AutoImportProviderProject.getRootFileNames( this.hostProject.includePackageJsonAutoImports(), this.hostProject, this.hostProject.getHostForAutoImportProvider(), this.getCompilationSettings(), ); } this.projectService.setFileNamesOfAutoImportProviderOrAuxillaryProject(this, rootFileNames); this.rootFileNames = rootFileNames; const oldProgram = this.getCurrentProgram(); const hasSameSetOfFiles = super.updateGraph(); if (oldProgram && oldProgram !== this.getCurrentProgram()) { this.hostProject.clearCachedExportInfoMap(); } return hasSameSetOfFiles; } /** @internal */ override scheduleInvalidateResolutionsOfFailedLookupLocations(): void { // Invalidation will happen on-demand as part of updateGraph return; } override hasRoots(): boolean { return !!this.rootFileNames?.length; } /** @internal */ override markAsDirty(): void { this.rootFileNames = undefined; super.markAsDirty(); } override getScriptFileNames(): string[] { return this.rootFileNames || ts.emptyArray; } override getLanguageService(): never { throw new Error("AutoImportProviderProject language service should never be used. To get the program, use `project.getCurrentProgram()`."); } /** @internal */ override onAutoImportProviderSettingsChanged(): never { throw new Error("AutoImportProviderProject is an auto import provider; use `markAsDirty()` instead."); } /** @internal */ override onPackageJsonChange(): never { throw new Error("package.json changes should be notified on an AutoImportProvider's host project"); } override getHostForAutoImportProvider(): never { throw new Error("AutoImportProviderProject cannot provide its own host; use `hostProject.getModuleResolutionHostForAutomImportProvider()` instead."); } override getProjectReferences(): readonly ProjectReference[] | undefined { return this.hostProject.getProjectReferences(); } /** @internal */ override includePackageJsonAutoImports(): PackageJsonAutoImportPreference { return PackageJsonAutoImportPreference.Off; } /** @internal */ override getSymlinkCache(): SymlinkCache { return this.hostProject.getSymlinkCache(); } /** @internal */ override getModuleResolutionCache(): ModuleResolutionCache | undefined { return this.hostProject.getCurrentProgram()?.getModuleResolutionCache(); } } /** * 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 { /** @internal */ pendingUpdateLevel: ProgramUpdateLevel; /** @internal */ pendingUpdateReason: string | undefined; /** @internal */ openFileWatchTriggered: Map = new Map(); /** @internal */ parsedCommandLine: ParsedCommandLine | undefined; /** @internal */ resolvedChildConfigs?: Set; private projectReferences: readonly ProjectReference[] | undefined; /** * Potential project references before the project is actually loaded (read config file) * * @internal */ potentialProjectReferences: Set | undefined; /** @internal */ projectOptions?: ProjectOptions | true; /** @internal */ override initialLoadPending = true; /** @internal */ sendLoadingProjectFinish = false; private compilerHost?: CompilerHost; /** @internal */ configDiagDiagnosticsReported?: number; /** @internal */ triggerFileForConfigFileDiag?: NormalizedPath; /** @internal */ deferredClose?: boolean; /** @internal */ constructor( configFileName: NormalizedPath, readonly canonicalConfigFilePath: NormalizedPath, projectService: ProjectService, cachedDirectoryStructureHost: CachedDirectoryStructureHost, pendingUpdateReason: string, ) { super( configFileName, ProjectKind.Configured, projectService, /*hasExplicitListOfFiles*/ false, /*lastFileExceededProgramSize*/ undefined, /*compilerOptions*/ {}, /*compileOnSaveEnabled*/ false, /*watchOptions*/ undefined, cachedDirectoryStructureHost, getDirectoryPath(configFileName), ); this.pendingUpdateLevel = ProgramUpdateLevel.Full; this.pendingUpdateReason = pendingUpdateReason; } /** @internal */ setCompilerHost(host: CompilerHost): void { this.compilerHost = host; } /** @internal */ getCompilerHost(): CompilerHost | undefined { return this.compilerHost; } /** @internal */ override useSourceOfProjectReferenceRedirect(): boolean { return this.languageServiceEnabled; } /** @internal */ override getParsedCommandLine(fileName: string): ParsedCommandLine | undefined { const configFileName = toNormalizedPath(fileName); const canonicalConfigFilePath = asNormalizedPath(this.projectService.toCanonicalFileName(configFileName)); // Ensure the config file existience info is cached let configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (!configFileExistenceInfo) { this.projectService.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo = { exists: this.projectService.host.fileExists(configFileName) }); } // Ensure we have upto date parsed command line this.projectService.ensureParsedConfigUptoDate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, this); // Watch wild cards if LS is enabled if (this.languageServiceEnabled && this.projectService.serverMode === LanguageServiceMode.Semantic) { this.projectService.watchWildcards(configFileName, configFileExistenceInfo, this); } return configFileExistenceInfo.exists ? configFileExistenceInfo.config!.parsedCommandLine : undefined; } /** @internal */ onReleaseParsedCommandLine(fileName: string): void { this.releaseParsedConfig(asNormalizedPath(this.projectService.toCanonicalFileName(toNormalizedPath(fileName)))); } private releaseParsedConfig(canonicalConfigFilePath: NormalizedPath) { this.projectService.stopWatchingWildCards(canonicalConfigFilePath, this); this.projectService.releaseParsedConfig(canonicalConfigFilePath, 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. */ override updateGraph(): boolean { if (this.deferredClose) return false; const isDirty = this.dirty; this.initialLoadPending = false; const updateLevel = this.pendingUpdateLevel; this.pendingUpdateLevel = ProgramUpdateLevel.Update; let result: boolean; switch (updateLevel) { case ProgramUpdateLevel.RootNamesAndUpdate: this.openFileWatchTriggered.clear(); result = this.projectService.reloadFileNamesOfConfiguredProject(this); break; case ProgramUpdateLevel.Full: this.openFileWatchTriggered.clear(); const reason = Debug.checkDefined(this.pendingUpdateReason); this.projectService.reloadConfiguredProject(this, reason); result = true; break; default: result = super.updateGraph(); } this.compilerHost = undefined; this.projectService.sendProjectLoadingFinishEvent(this); this.projectService.sendProjectTelemetry(this); if ( updateLevel === ProgramUpdateLevel.Full || ( // Already sent event through reload result && ( // Not new program !isDirty || !this.triggerFileForConfigFileDiag || this.getCurrentProgram()!.structureIsReused === StructureIsReused.Completely ) ) ) { // Dont send the configFileDiag this.triggerFileForConfigFileDiag = undefined; } else if (!this.triggerFileForConfigFileDiag) { // If we arent tracking to send configFileDiag, send event if diagnostics presence has changed this.projectService.sendConfigFileDiagEvent(this, /*triggerFile*/ undefined, /*force*/ false); } return result; } /** @internal */ override getCachedDirectoryStructureHost() { return this.directoryStructureHost as CachedDirectoryStructureHost; } getConfigFilePath(): NormalizedPath { return asNormalizedPath(this.getProjectName()); } override getProjectReferences(): readonly ProjectReference[] | undefined { return this.projectReferences; } updateReferences(refs: readonly ProjectReference[] | undefined): void { this.projectReferences = refs; this.potentialProjectReferences = undefined; } /** @internal */ setPotentialProjectReference(canonicalConfigPath: NormalizedPath): void { Debug.assert(this.initialLoadPending); (this.potentialProjectReferences || (this.potentialProjectReferences = new Set())).add(canonicalConfigPath); } /** @internal */ override getRedirectFromSourceFile(fileName: string): ResolvedRefAndOutputDts | undefined { const program = this.getCurrentProgram(); return program && program.getRedirectFromSourceFile(fileName); } /** @internal */ forEachResolvedProjectReference( cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined, ): T | undefined { return this.getCurrentProgram()?.forEachResolvedProjectReference(cb); } /** @internal */ enablePluginsWithOptions(options: CompilerOptions): void { this.plugins.length = 0; if (!options.plugins?.length && !this.projectService.globalPlugins.length) return; const host = this.projectService.host; if (!host.require && !host.importPlugin) { this.projectService.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded"); return; } const searchPaths = this.getGlobalPluginSearchPaths(); 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); } } return this.enableGlobalPlugins(options); } /** * Get the errors that dont have any file name associated */ override getGlobalProjectErrors(): readonly Diagnostic[] { return filter(this.projectErrors, diagnostic => !diagnostic.file) || emptyArray; } /** * Get all the project errors */ override getAllProjectErrors(): readonly Diagnostic[] { return this.projectErrors || emptyArray; } override setProjectErrors(projectErrors: Diagnostic[]): void { this.projectErrors = projectErrors; } override close(): void { this.projectService.configFileExistenceInfoCache.forEach((_configFileExistenceInfo, canonicalConfigFilePath) => this.releaseParsedConfig(canonicalConfigFilePath)); this.projectErrors = undefined; this.openFileWatchTriggered.clear(); this.compilerHost = undefined; super.close(); } /** @internal */ override markAsDirty(): void { if (this.deferredClose) return; super.markAsDirty(); } /** @internal */ override isOrphan(): boolean { return !!this.deferredClose; } getEffectiveTypeRoots(): string[] { return getEffectiveTypeRoots(this.getCompilationSettings(), this) || []; } /** @internal */ updateErrorOnNoInputFiles(parsedCommandLine: ParsedCommandLine): void { this.parsedCommandLine = parsedCommandLine; updateErrorForNoInputFiles( parsedCommandLine.fileNames, this.getConfigFilePath(), this.getCompilerOptions().configFile!.configFileSpecs!, this.projectErrors!, canJsonReportNoInputFiles(parsedCommandLine.raw), ); } } /** * 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[] = []; /** @internal */ constructor( public externalProjectName: string, projectService: ProjectService, compilerOptions: CompilerOptions, lastFileExceededProgramSize: string | undefined, public override compileOnSaveEnabled: boolean, projectFilePath: string | undefined, watchOptions: WatchOptions | undefined, ) { super( externalProjectName, ProjectKind.External, projectService, /*hasExplicitListOfFiles*/ true, lastFileExceededProgramSize, compilerOptions, compileOnSaveEnabled, watchOptions, projectService.host, getDirectoryPath(projectFilePath || normalizeSlashes(externalProjectName)), ); this.enableGlobalPlugins(this.getCompilerOptions()); } override updateGraph(): boolean { const result = super.updateGraph(); this.projectService.sendProjectTelemetry(this); return result; } override getExcludedFiles(): readonly NormalizedPath[] { return this.excludedFiles; } } /** @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; } /**@internal */ export function isBackgroundProject(project: Project): project is AutoImportProviderProject | AuxiliaryProject { return project.projectKind === ProjectKind.AutoImportProvider || project.projectKind === ProjectKind.Auxiliary; } /** @internal */ export function isProjectDeferredClose(project: Project): project is ConfiguredProject { return isConfiguredProject(project) && !!project.deferredClose; }