1import { 2 ActionInvalidate, ActionSet, asNormalizedPath, AutoImportProviderProject, BeginEnablePluginResult, 3 BeginInstallTypes, ConfiguredProject, countEachFileTypes, createPackageJsonCache, emptyArray, EndInstallTypes, 4 Errors, ExternalProject, getBaseConfigFileName, hasNoTypeScriptSource, InferredProject, InvalidateCachedTypings, 5 isConfiguredProject, isDynamicFileName, isInferredProject, isInferredProjectName, ITypingsInstaller, Logger, 6 LogLevel, makeAutoImportProviderProjectName, makeAuxiliaryProjectName, makeInferredProjectName, Msg, NormalizedPath, 7 normalizedPathToPath, nullTypingsInstaller, PackageInstalledResponse, PackageJsonCache, Project, 8 ProjectFilesWithTSDiagnostics, ProjectKind, ProjectOptions, protocol, ScriptInfo, ScriptInfoVersion, ServerHost, 9 Session, SetTypings, ThrottledOperations, toNormalizedPath, TypingsCache, 10} from "./_namespaces/ts.server"; 11import { 12 addToSeen, arrayFrom, arrayToMap, AssertionLevel, CachedDirectoryStructureHost, canJsonReportNoInputFiles, 13 canWatchDirectoryOrFile, cleanExtendedConfigCache, clearMap, clearSharedExtendedConfigFileWatcher, 14 closeFileWatcherOf, combinePaths, CommandLineOption, CompilerOptions, CompletionInfo, ConfigFileProgramReloadLevel, 15 contains, containsPath, convertCompilerOptionsForTelemetry, convertEnableAutoDiscoveryToEnable, convertJsonOption, 16 createCachedDirectoryStructureHost, createDocumentRegistryInternal, createGetCanonicalFileName, createMultiMap, 17 Debug, Diagnostic, directorySeparator, DirectoryStructureHost, DocumentPosition, DocumentPositionMapper, 18 DocumentRegistry, DocumentRegistryBucketKeyWithMode, emptyOptions, ensureTrailingDirectorySeparator, ESMap, 19 ExtendedConfigCacheEntry, FileExtensionInfo, fileExtensionIs, FileWatcher, FileWatcherEventKind, find, flatMap, 20 forEach, forEachAncestorDirectory, forEachEntry, forEachKey, forEachResolvedProjectReference, FormatCodeSettings, 21 getAnyExtensionFromPath, getBaseFileName, getDefaultFormatCodeSettings, getDirectoryPath, getDocumentPositionMapper, 22 getEntries, getFileNamesFromConfigSpecs, getFileWatcherEventKind, getNormalizedAbsolutePath, getSnapshotText, 23 getWatchFactory, hasExtension, hasProperty, hasTSFileExtension, HostCancellationToken, identity, 24 IncompleteCompletionsCache, IndentStyle, isArray, isIgnoredFileFromWildCardWatching, isInsideNodeModules, 25 isJsonEqual, isNodeModulesDirectory, isRootedDiskPath, isString, Iterator, LanguageServiceMode, length, map, Map, 26 mapDefinedEntries, mapDefinedIterator, missingFileModifiedTime, MultiMap, noop, normalizePath, normalizeSlashes, 27 optionDeclarations, optionsForWatch, PackageJsonAutoImportPreference, ParsedCommandLine, 28 parseJsonSourceFileConfigFileContent, parseJsonText, parsePackageName, Path, PerformanceEvent, PluginImport, 29 PollingInterval, ProjectPackageJsonInfo, ProjectReference, ReadMapFile, ReadonlyCollection, removeFileExtension, 30 removeIgnoredPath, removeMinAndVersionNumbers, ResolvedProjectReference, resolveProjectReferencePath, 31 returnNoopFileWatcher, returnTrue, ScriptKind, Set, SharedExtendedConfigFileWatcher, some, SourceFile, SourceFileLike, startsWith, 32 Ternary, TextChange, toFileNameLowerCase, toPath, tracing, tryAddToSet, tryReadFile, TsConfigSourceFile, 33 TypeAcquisition, typeAcquisitionDeclarations, unorderedRemoveItem, updateSharedExtendedConfigFileWatcher, 34 updateWatchingWildcardDirectories, UserPreferences, version, WatchDirectoryFlags, WatchFactory, WatchLogLevel, 35 WatchOptions, WatchType, WildcardDirectoryWatcher, 36} from "./_namespaces/ts"; 37 38export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; 39/** @internal */ 40export const maxFileSize = 4 * 1024 * 1024; 41 42export const ProjectsUpdatedInBackgroundEvent = "projectsUpdatedInBackground"; 43export const ProjectLoadingStartEvent = "projectLoadingStart"; 44export const ProjectLoadingFinishEvent = "projectLoadingFinish"; 45export const LargeFileReferencedEvent = "largeFileReferenced"; 46export const ConfigFileDiagEvent = "configFileDiag"; 47export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; 48export const ProjectInfoTelemetryEvent = "projectInfo"; 49export const OpenFileInfoTelemetryEvent = "openFileInfo"; 50const ensureProjectForOpenFileSchedule = "*ensureProjectForOpenFiles*"; 51 52export interface ProjectsUpdatedInBackgroundEvent { 53 eventName: typeof ProjectsUpdatedInBackgroundEvent; 54 data: { openFiles: string[]; }; 55} 56 57export interface ProjectLoadingStartEvent { 58 eventName: typeof ProjectLoadingStartEvent; 59 data: { project: Project; reason: string; }; 60} 61 62export interface ProjectLoadingFinishEvent { 63 eventName: typeof ProjectLoadingFinishEvent; 64 data: { project: Project; }; 65} 66 67export interface LargeFileReferencedEvent { 68 eventName: typeof LargeFileReferencedEvent; 69 data: { file: string; fileSize: number; maxFileSize: number; }; 70} 71 72export interface ConfigFileDiagEvent { 73 eventName: typeof ConfigFileDiagEvent; 74 data: { triggerFile: string, configFileName: string, diagnostics: readonly Diagnostic[] }; 75} 76 77export interface ProjectLanguageServiceStateEvent { 78 eventName: typeof ProjectLanguageServiceStateEvent; 79 data: { project: Project, languageServiceEnabled: boolean }; 80} 81 82/** This will be converted to the payload of a protocol.TelemetryEvent in session.defaultEventHandler. */ 83export interface ProjectInfoTelemetryEvent { 84 readonly eventName: typeof ProjectInfoTelemetryEvent; 85 readonly data: ProjectInfoTelemetryEventData; 86} 87 88/* __GDPR__ 89 "projectInfo" : { 90 "${include}": ["${TypeScriptCommonProperties}"], 91 "projectId": { "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight", "endpoint": "ProjectId" }, 92 "fileStats": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 93 "compilerOptions": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 94 "extends": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 95 "files": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 96 "include": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 97 "exclude": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 98 "compileOnSave": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 99 "typeAcquisition": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 100 "configFileName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 101 "projectType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 102 "languageServiceEnabled": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, 103 "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } 104 } 105 */ 106export interface ProjectInfoTelemetryEventData { 107 /** Cryptographically secure hash of project file location. */ 108 readonly projectId: string; 109 /** Count of file extensions seen in the project. */ 110 readonly fileStats: FileStats; 111 /** 112 * Any compiler options that might contain paths will be taken out. 113 * Enum compiler options will be converted to strings. 114 */ 115 readonly compilerOptions: CompilerOptions; 116 // "extends", "files", "include", or "exclude" will be undefined if an external config is used. 117 // Otherwise, we will use "true" if the property is present and "false" if it is missing. 118 readonly extends: boolean | undefined; 119 readonly files: boolean | undefined; 120 readonly include: boolean | undefined; 121 readonly exclude: boolean | undefined; 122 readonly compileOnSave: boolean; 123 readonly typeAcquisition: ProjectInfoTypeAcquisitionData; 124 125 readonly configFileName: "tsconfig.json" | "jsconfig.json" | "other"; 126 readonly projectType: "external" | "configured"; 127 readonly languageServiceEnabled: boolean; 128 /** TypeScript version used by the server. */ 129 readonly version: string; 130} 131 132/** 133 * Info that we may send about a file that was just opened. 134 * Info about a file will only be sent once per session, even if the file changes in ways that might affect the info. 135 * Currently this is only sent for '.js' files. 136 */ 137export interface OpenFileInfoTelemetryEvent { 138 readonly eventName: typeof OpenFileInfoTelemetryEvent; 139 readonly data: OpenFileInfoTelemetryEventData; 140} 141 142export interface OpenFileInfoTelemetryEventData { 143 readonly info: OpenFileInfo; 144} 145 146export interface ProjectInfoTypeAcquisitionData { 147 readonly enable: boolean | undefined; 148 // Actual values of include/exclude entries are scrubbed. 149 readonly include: boolean; 150 readonly exclude: boolean; 151} 152 153export interface FileStats { 154 readonly js: number; 155 readonly jsSize?: number; 156 157 readonly jsx: number; 158 readonly jsxSize?: number; 159 160 readonly ts: number; 161 readonly tsSize?: number; 162 163 readonly tsx: number; 164 readonly tsxSize?: number; 165 166 readonly dts: number; 167 readonly dtsSize?: number; 168 169 readonly deferred: number; 170 readonly deferredSize?: number; 171 172 readonly ets: number; 173 readonly etsSize?: number; 174 175 readonly dets: number; 176 readonly detsSize?: number; 177} 178 179export interface OpenFileInfo { 180 readonly checkJs: boolean; 181} 182 183export type ProjectServiceEvent = 184 LargeFileReferencedEvent 185 | ProjectsUpdatedInBackgroundEvent 186 | ProjectLoadingStartEvent 187 | ProjectLoadingFinishEvent 188 | ConfigFileDiagEvent 189 | ProjectLanguageServiceStateEvent 190 | ProjectInfoTelemetryEvent 191 | OpenFileInfoTelemetryEvent; 192 193export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; 194 195/** @internal */ 196export type PerformanceEventHandler = (event: PerformanceEvent) => void; 197 198export interface SafeList { 199 [name: string]: { match: RegExp, exclude?: (string | number)[][], types?: string[] }; 200} 201 202function prepareConvertersForEnumLikeCompilerOptions(commandLineOptions: CommandLineOption[]): ESMap<string, ESMap<string, number>> { 203 const map = new Map<string, ESMap<string, number>>(); 204 for (const option of commandLineOptions) { 205 if (typeof option.type === "object") { 206 const optionMap = option.type as ESMap<string, number>; 207 // verify that map contains only numbers 208 optionMap.forEach(value => { 209 Debug.assert(typeof value === "number"); 210 }); 211 map.set(option.name, optionMap); 212 } 213 } 214 return map; 215} 216 217const compilerOptionConverters = prepareConvertersForEnumLikeCompilerOptions(optionDeclarations); 218const watchOptionsConverters = prepareConvertersForEnumLikeCompilerOptions(optionsForWatch); 219const indentStyle = new Map(getEntries({ 220 none: IndentStyle.None, 221 block: IndentStyle.Block, 222 smart: IndentStyle.Smart 223})); 224 225export interface TypesMapFile { 226 typesMap: SafeList; 227 simpleMap: { [libName: string]: string }; 228} 229 230/** 231 * How to understand this block: 232 * * The 'match' property is a regexp that matches a filename. 233 * * If 'match' is successful, then: 234 * * All files from 'exclude' are removed from the project. See below. 235 * * All 'types' are included in ATA 236 * * What the heck is 'exclude' ? 237 * * An array of an array of strings and numbers 238 * * Each array is: 239 * * An array of strings and numbers 240 * * The strings are literals 241 * * The numbers refer to capture group indices from the 'match' regexp 242 * * Remember that '1' is the first group 243 * * These are concatenated together to form a new regexp 244 * * Filenames matching these regexps are excluded from the project 245 * This default value is tested in tsserverProjectSystem.ts; add tests there 246 * if you are changing this so that you can be sure your regexp works! 247 */ 248const defaultTypeSafeList: SafeList = { 249 "jquery": { 250 // jquery files can have names like "jquery-1.10.2.min.js" (or "jquery.intellisense.js") 251 match: /jquery(-[\d\.]+)?(\.intellisense)?(\.min)?\.js$/i, 252 types: ["jquery"] 253 }, 254 "WinJS": { 255 // e.g. c:/temp/UWApp1/lib/winjs-4.0.1/js/base.js 256 match: /^(.*\/winjs-[.\d]+)\/js\/base\.js$/i, // If the winjs/base.js file is found.. 257 exclude: [["^", 1, "/.*"]], // ..then exclude all files under the winjs folder 258 types: ["winjs"] // And fetch the @types package for WinJS 259 }, 260 "Kendo": { 261 // e.g. /Kendo3/wwwroot/lib/kendo/kendo.all.min.js 262 match: /^(.*\/kendo(-ui)?)\/kendo\.all(\.min)?\.js$/i, 263 exclude: [["^", 1, "/.*"]], 264 types: ["kendo-ui"] 265 }, 266 "Office Nuget": { 267 // e.g. /scripts/Office/1/excel-15.debug.js 268 match: /^(.*\/office\/1)\/excel-\d+\.debug\.js$/i, // Office NuGet package is installed under a "1/office" folder 269 exclude: [["^", 1, "/.*"]], // Exclude that whole folder if the file indicated above is found in it 270 types: ["office"] // @types package to fetch instead 271 }, 272 "References": { 273 match: /^(.*\/_references\.js)$/i, 274 exclude: [["^", 1, "$"]] 275 } 276}; 277 278export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings { 279 if (isString(protocolOptions.indentStyle)) { 280 protocolOptions.indentStyle = indentStyle.get(protocolOptions.indentStyle.toLowerCase()); 281 Debug.assert(protocolOptions.indentStyle !== undefined); 282 } 283 return protocolOptions as any; 284} 285 286export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin { 287 compilerOptionConverters.forEach((mappedValues, id) => { 288 const propertyValue = protocolOptions[id]; 289 if (isString(propertyValue)) { 290 protocolOptions[id] = mappedValues.get(propertyValue.toLowerCase()); 291 } 292 }); 293 return protocolOptions as any; 294} 295 296export function convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions, currentDirectory?: string): WatchOptionsAndErrors | undefined { 297 let watchOptions: WatchOptions | undefined; 298 let errors: Diagnostic[] | undefined; 299 optionsForWatch.forEach(option => { 300 const propertyValue = protocolOptions[option.name]; 301 if (propertyValue === undefined) return; 302 const mappedValues = watchOptionsConverters.get(option.name); 303 (watchOptions || (watchOptions = {}))[option.name] = mappedValues ? 304 isString(propertyValue) ? mappedValues.get(propertyValue.toLowerCase()) : propertyValue : 305 convertJsonOption(option, propertyValue, currentDirectory || "", errors || (errors = [])); 306 }); 307 return watchOptions && { watchOptions, errors }; 308} 309 310export function convertTypeAcquisition(protocolOptions: protocol.InferredProjectCompilerOptions): TypeAcquisition | undefined { 311 let result: TypeAcquisition | undefined; 312 typeAcquisitionDeclarations.forEach((option) => { 313 const propertyValue = protocolOptions[option.name]; 314 if (propertyValue === undefined) return; 315 (result || (result = {}))[option.name] = propertyValue; 316 }); 317 return result; 318} 319 320export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind { 321 return isString(scriptKindName) ? convertScriptKindName(scriptKindName) : scriptKindName; 322} 323 324export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) { 325 switch (scriptKindName) { 326 case "JS": 327 return ScriptKind.JS; 328 case "JSX": 329 return ScriptKind.JSX; 330 case "TS": 331 return ScriptKind.TS; 332 case "TSX": 333 return ScriptKind.TSX; 334 case "ETS": 335 return ScriptKind.ETS; 336 default: 337 return ScriptKind.Unknown; 338 } 339} 340 341/** @internal */ 342export function convertUserPreferences(preferences: protocol.UserPreferences): UserPreferences { 343 const { lazyConfiguredProjectsFromExternalProject, ...userPreferences } = preferences; 344 return userPreferences; 345} 346 347export interface HostConfiguration { 348 formatCodeOptions: FormatCodeSettings; 349 preferences: protocol.UserPreferences; 350 hostInfo: string; 351 extraFileExtensions?: FileExtensionInfo[]; 352 watchOptions?: WatchOptions; 353} 354 355export interface OpenConfiguredProjectResult { 356 configFileName?: NormalizedPath; 357 configFileErrors?: readonly Diagnostic[]; 358} 359 360interface AssignProjectResult extends OpenConfiguredProjectResult { 361 retainProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined; 362} 363 364interface FilePropertyReader<T> { 365 getFileName(f: T): string; 366 getScriptKind(f: T, extraFileExtensions?: FileExtensionInfo[]): ScriptKind; 367 hasMixedContent(f: T, extraFileExtensions: FileExtensionInfo[] | undefined): boolean; 368} 369 370const fileNamePropertyReader: FilePropertyReader<string> = { 371 getFileName: x => x, 372 getScriptKind: (fileName, extraFileExtensions) => { 373 let result: ScriptKind | undefined; 374 if (extraFileExtensions) { 375 const fileExtension = getAnyExtensionFromPath(fileName); 376 if (fileExtension) { 377 some(extraFileExtensions, info => { 378 if (info.extension === fileExtension) { 379 result = info.scriptKind; 380 return true; 381 } 382 return false; 383 }); 384 } 385 } 386 return result!; // TODO: GH#18217 387 }, 388 hasMixedContent: (fileName, extraFileExtensions) => some(extraFileExtensions, ext => ext.isMixedContent && fileExtensionIs(fileName, ext.extension)), 389}; 390 391const externalFilePropertyReader: FilePropertyReader<protocol.ExternalFile> = { 392 getFileName: x => x.fileName, 393 getScriptKind: x => tryConvertScriptKindName(x.scriptKind!), // TODO: GH#18217 394 hasMixedContent: x => !!x.hasMixedContent, 395}; 396 397function findProjectByName<T extends Project>(projectName: string, projects: T[]): T | undefined { 398 for (const proj of projects) { 399 if (proj.getProjectName() === projectName) { 400 return proj; 401 } 402 } 403} 404 405const noopConfigFileWatcher: FileWatcher = { close: noop }; 406 407/** @internal */ 408export interface ConfigFileExistenceInfo { 409 /** 410 * Cached value of existence of config file 411 * It is true if there is configured project open for this file. 412 * It can be either true or false if this is the config file that is being watched by inferred project 413 * to decide when to update the structure so that it knows about updating the project for its files 414 * (config file may include the inferred project files after the change and hence may be wont need to be in inferred project) 415 */ 416 exists: boolean; 417 /** 418 * openFilesImpactedByConfigFiles is a map of open files that would be impacted by this config file 419 * because these are the paths being looked up for their default configured project location 420 * The value in the map is true if the open file is root of the inferred project 421 * It is false when the open file that would still be impacted by existence of 422 * this config file but it is not the root of inferred project 423 */ 424 openFilesImpactedByConfigFile?: ESMap<Path, boolean>; 425 /** 426 * The file watcher watching the config file because there is open script info that is root of 427 * inferred project and will be impacted by change in the status of the config file 428 * or 429 * Configured project for this config file is open 430 * or 431 * Configured project references this config file 432 */ 433 watcher?: FileWatcher; 434 /** 435 * Cached parsed command line and other related information like watched directories etc 436 */ 437 config?: ParsedConfig; 438} 439 440export interface ProjectServiceOptions { 441 host: ServerHost; 442 logger: Logger; 443 cancellationToken: HostCancellationToken; 444 useSingleInferredProject: boolean; 445 useInferredProjectPerProjectRoot: boolean; 446 typingsInstaller: ITypingsInstaller; 447 eventHandler?: ProjectServiceEventHandler; 448 suppressDiagnosticEvents?: boolean; 449 throttleWaitMilliseconds?: number; 450 globalPlugins?: readonly string[]; 451 pluginProbeLocations?: readonly string[]; 452 allowLocalPluginLoads?: boolean; 453 typesMapLocation?: string; 454 /** @deprecated use serverMode instead */ 455 syntaxOnly?: boolean; 456 serverMode?: LanguageServiceMode; 457 session: Session<unknown> | undefined; 458} 459 460interface OriginalFileInfo { fileName: NormalizedPath; path: Path; } 461interface AncestorConfigFileInfo { 462 /** config file name */ 463 fileName: string; 464 /** path of open file so we can look at correct root */ 465 path: Path; 466 configFileInfo: true; 467} 468type OpenScriptInfoOrClosedFileInfo = ScriptInfo | OriginalFileInfo; 469type OpenScriptInfoOrClosedOrConfigFileInfo = OpenScriptInfoOrClosedFileInfo | AncestorConfigFileInfo; 470 471function isOpenScriptInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is ScriptInfo { 472 return !!(infoOrFileNameOrConfig as ScriptInfo).containingProjects; 473} 474 475function isAncestorConfigFileInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is AncestorConfigFileInfo { 476 return !!(infoOrFileNameOrConfig as AncestorConfigFileInfo).configFileInfo; 477} 478 479/** 480 * Kind of operation to perform to get project reference project 481 * 482 * @internal 483 */ 484export enum ProjectReferenceProjectLoadKind { 485 /** Find existing project for project reference */ 486 Find, 487 /** Find existing project or create one for the project reference */ 488 FindCreate, 489 /** Find existing project or create and load it for the project reference */ 490 FindCreateLoad 491} 492 493/** @internal */ 494export function forEachResolvedProjectReferenceProject<T>( 495 project: ConfiguredProject, 496 fileName: string | undefined, 497 cb: (child: ConfiguredProject) => T | undefined, 498 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.Find | ProjectReferenceProjectLoadKind.FindCreate, 499): T | undefined; 500/** @internal */ 501export function forEachResolvedProjectReferenceProject<T>( 502 project: ConfiguredProject, 503 fileName: string | undefined, 504 cb: (child: ConfiguredProject) => T | undefined, 505 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, 506 reason: string 507): T | undefined; 508export function forEachResolvedProjectReferenceProject<T>( 509 project: ConfiguredProject, 510 fileName: string | undefined, 511 cb: (child: ConfiguredProject) => T | undefined, 512 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, 513 reason?: string 514): T | undefined { 515 const resolvedRefs = project.getCurrentProgram()?.getResolvedProjectReferences(); 516 if (!resolvedRefs) return undefined; 517 let seenResolvedRefs: ESMap<string, ProjectReferenceProjectLoadKind> | undefined; 518 const possibleDefaultRef = fileName ? project.getResolvedProjectReferenceToRedirect(fileName) : undefined; 519 if (possibleDefaultRef) { 520 // Try to find the name of the file directly through resolved project references 521 const configFileName = toNormalizedPath(possibleDefaultRef.sourceFile.fileName); 522 const child = project.projectService.findConfiguredProjectByProjectName(configFileName); 523 if (child) { 524 const result = cb(child); 525 if (result) return result; 526 } 527 else if (projectReferenceProjectLoadKind !== ProjectReferenceProjectLoadKind.Find) { 528 seenResolvedRefs = new Map(); 529 // Try to see if this project can be loaded 530 const result = forEachResolvedProjectReferenceProjectWorker( 531 resolvedRefs, 532 project.getCompilerOptions(), 533 (ref, loadKind) => possibleDefaultRef === ref ? callback(ref, loadKind) : undefined, 534 projectReferenceProjectLoadKind, 535 project.projectService, 536 seenResolvedRefs 537 ); 538 if (result) return result; 539 // Cleanup seenResolvedRefs 540 seenResolvedRefs.clear(); 541 } 542 } 543 544 return forEachResolvedProjectReferenceProjectWorker( 545 resolvedRefs, 546 project.getCompilerOptions(), 547 (ref, loadKind) => possibleDefaultRef !== ref ? callback(ref, loadKind) : undefined, 548 projectReferenceProjectLoadKind, 549 project.projectService, 550 seenResolvedRefs 551 ); 552 553 function callback(ref: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) { 554 const configFileName = toNormalizedPath(ref.sourceFile.fileName); 555 const child = project.projectService.findConfiguredProjectByProjectName(configFileName) || ( 556 loadKind === ProjectReferenceProjectLoadKind.Find ? 557 undefined : 558 loadKind === ProjectReferenceProjectLoadKind.FindCreate ? 559 project.projectService.createConfiguredProject(configFileName) : 560 loadKind === ProjectReferenceProjectLoadKind.FindCreateLoad ? 561 project.projectService.createAndLoadConfiguredProject(configFileName, reason!) : 562 Debug.assertNever(loadKind) 563 ); 564 565 return child && cb(child); 566 } 567} 568 569function forEachResolvedProjectReferenceProjectWorker<T>( 570 resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[], 571 parentOptions: CompilerOptions, 572 cb: (resolvedRef: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) => T | undefined, 573 projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, 574 projectService: ProjectService, 575 seenResolvedRefs: ESMap<string, ProjectReferenceProjectLoadKind> | undefined, 576): T | undefined { 577 const loadKind = parentOptions.disableReferencedProjectLoad ? ProjectReferenceProjectLoadKind.Find : projectReferenceProjectLoadKind; 578 return forEach(resolvedProjectReferences, ref => { 579 if (!ref) return undefined; 580 581 const configFileName = toNormalizedPath(ref.sourceFile.fileName); 582 const canonicalPath = projectService.toCanonicalFileName(configFileName); 583 const seenValue = seenResolvedRefs?.get(canonicalPath); 584 if (seenValue !== undefined && seenValue >= loadKind) { 585 return undefined; 586 } 587 const result = cb(ref, loadKind); 588 if (result) { 589 return result; 590 } 591 592 (seenResolvedRefs || (seenResolvedRefs = new Map())).set(canonicalPath, loadKind); 593 return ref.references && forEachResolvedProjectReferenceProjectWorker(ref.references, ref.commandLine.options, cb, loadKind, projectService, seenResolvedRefs); 594 }); 595} 596 597function forEachPotentialProjectReference<T>( 598 project: ConfiguredProject, 599 cb: (potentialProjectReference: Path) => T | undefined 600): T | undefined { 601 return project.potentialProjectReferences && 602 forEachKey(project.potentialProjectReferences, cb); 603} 604 605function forEachAnyProjectReferenceKind<T>( 606 project: ConfiguredProject, 607 cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined, 608 cbProjectRef: (projectReference: ProjectReference) => T | undefined, 609 cbPotentialProjectRef: (potentialProjectReference: Path) => T | undefined 610): T | undefined { 611 return project.getCurrentProgram() ? 612 project.forEachResolvedProjectReference(cb) : 613 project.isInitialLoadPending() ? 614 forEachPotentialProjectReference(project, cbPotentialProjectRef) : 615 forEach(project.getProjectReferences(), cbProjectRef); 616} 617 618function callbackRefProject<T>( 619 project: ConfiguredProject, 620 cb: (refProj: ConfiguredProject) => T | undefined, 621 refPath: Path | undefined 622) { 623 const refProject = refPath && project.projectService.configuredProjects.get(refPath); 624 return refProject && cb(refProject); 625} 626 627function forEachReferencedProject<T>( 628 project: ConfiguredProject, 629 cb: (refProj: ConfiguredProject) => T | undefined 630): T | undefined { 631 return forEachAnyProjectReferenceKind( 632 project, 633 resolvedRef => callbackRefProject(project, cb, resolvedRef.sourceFile.path), 634 projectRef => callbackRefProject(project, cb, project.toPath(resolveProjectReferencePath(projectRef))), 635 potentialProjectRef => callbackRefProject(project, cb, potentialProjectRef) 636 ); 637} 638 639interface NodeModulesWatcher extends FileWatcher { 640 /** How many watchers of this directory were for closed ScriptInfo */ 641 refreshScriptInfoRefCount: number; 642 /** List of project names whose module specifier cache should be cleared when package.jsons change */ 643 affectedModuleSpecifierCacheProjects?: Set<string>; 644} 645 646function getDetailWatchInfo(watchType: WatchType, project: Project | NormalizedPath | undefined) { 647 return `${isString(project) ? `Config: ${project} ` : project ? `Project: ${project.getProjectName()} ` : ""}WatchType: ${watchType}`; 648} 649 650function isScriptInfoWatchedFromNodeModules(info: ScriptInfo) { 651 return !info.isScriptOpen() && info.mTime !== undefined; 652} 653 654/** 655 * true if script info is part of project and is not in project because it is referenced from project reference source 656 * 657 * @internal 658 */ 659export function projectContainsInfoDirectly(project: Project, info: ScriptInfo) { 660 return project.containsScriptInfo(info) && 661 !project.isSourceOfProjectReferenceRedirect(info.path); 662} 663 664/** @internal */ 665export function updateProjectIfDirty(project: Project) { 666 project.invalidateResolutionsOfFailedLookupLocations(); 667 return project.dirty && project.updateGraph(); 668} 669 670function setProjectOptionsUsed(project: ConfiguredProject | ExternalProject) { 671 if (isConfiguredProject(project)) { 672 project.projectOptions = true; 673 } 674} 675 676/** @internal */ 677export interface OpenFileArguments { 678 fileName: string; 679 content?: string; 680 scriptKind?: protocol.ScriptKindName | ScriptKind; 681 hasMixedContent?: boolean; 682 projectRootPath?: string; 683} 684 685/** @internal */ 686export interface ChangeFileArguments { 687 fileName: string; 688 changes: Iterator<TextChange>; 689} 690 691export interface WatchOptionsAndErrors { 692 watchOptions: WatchOptions; 693 errors: Diagnostic[] | undefined; 694} 695 696/** @internal */ 697export interface ParsedConfig{ 698 cachedDirectoryStructureHost: CachedDirectoryStructureHost; 699 /** 700 * The map contains 701 * - true if project is watching config file as well as wild cards 702 * - false if just config file is watched 703 */ 704 projects: ESMap<NormalizedPath, boolean>; 705 parsedCommandLine?: ParsedCommandLine; 706 watchedDirectories?: Map<WildcardDirectoryWatcher>; 707 /** 708 * true if watchedDirectories need to be updated as per parsedCommandLine's updated watched directories 709 */ 710 watchedDirectoriesStale?: boolean; 711 reloadLevel?: ConfigFileProgramReloadLevel.Partial | ConfigFileProgramReloadLevel.Full; 712} 713 714function createProjectNameFactoryWithCounter(nameFactory: (counter: number) => string) { 715 let nextId = 1; 716 return () => nameFactory(nextId++); 717} 718 719export class ProjectService { 720 721 /** @internal */ 722 readonly typingsCache: TypingsCache; 723 724 /** @internal */ 725 readonly documentRegistry: DocumentRegistry; 726 727 /** 728 * Container of all known scripts 729 * 730 * @internal 731 */ 732 readonly filenameToScriptInfo = new Map<string, ScriptInfo>(); 733 private readonly nodeModulesWatchers = new Map<string, NodeModulesWatcher>(); 734 /** 735 * Contains all the deleted script info's version information so that 736 * it does not reset when creating script info again 737 * (and could have potentially collided with version where contents mismatch) 738 */ 739 private readonly filenameToScriptInfoVersion = new Map<string, ScriptInfoVersion>(); 740 // Set of all '.js' files ever opened. 741 private readonly allJsFilesForOpenFileTelemetry = new Map<string, true>(); 742 743 /** 744 * Map to the real path of the infos 745 * 746 * @internal 747 */ 748 readonly realpathToScriptInfos: MultiMap<Path, ScriptInfo> | undefined; 749 /** 750 * maps external project file name to list of config files that were the part of this project 751 */ 752 private readonly externalProjectToConfiguredProjectMap = new Map<string, NormalizedPath[]>(); 753 754 /** 755 * external projects (configuration and list of root files is not controlled by tsserver) 756 */ 757 readonly externalProjects: ExternalProject[] = []; 758 /** 759 * projects built from openFileRoots 760 */ 761 readonly inferredProjects: InferredProject[] = []; 762 /** 763 * projects specified by a tsconfig.json file 764 */ 765 readonly configuredProjects: Map<ConfiguredProject> = new Map<string, ConfiguredProject>(); 766 /** @internal */ 767 readonly newInferredProjectName = createProjectNameFactoryWithCounter(makeInferredProjectName); 768 /** @internal */ 769 readonly newAutoImportProviderProjectName = createProjectNameFactoryWithCounter(makeAutoImportProviderProjectName); 770 /** @internal */ 771 readonly newAuxiliaryProjectName = createProjectNameFactoryWithCounter(makeAuxiliaryProjectName); 772 /** 773 * Open files: with value being project root path, and key being Path of the file that is open 774 */ 775 readonly openFiles: Map<NormalizedPath | undefined> = new Map<Path, NormalizedPath | undefined>(); 776 /** @internal */ 777 readonly configFileForOpenFiles: ESMap<Path, NormalizedPath | false> = new Map(); 778 /** 779 * Map of open files that are opened without complete path but have projectRoot as current directory 780 */ 781 private readonly openFilesWithNonRootedDiskPath = new Map<string, ScriptInfo>(); 782 783 private compilerOptionsForInferredProjects: CompilerOptions | undefined; 784 private compilerOptionsForInferredProjectsPerProjectRoot = new Map<string, CompilerOptions>(); 785 private watchOptionsForInferredProjects: WatchOptionsAndErrors | undefined; 786 private watchOptionsForInferredProjectsPerProjectRoot = new Map<string, WatchOptionsAndErrors | false>(); 787 private typeAcquisitionForInferredProjects: TypeAcquisition | undefined; 788 private typeAcquisitionForInferredProjectsPerProjectRoot = new Map<string, TypeAcquisition | undefined>(); 789 /** 790 * Project size for configured or external projects 791 */ 792 private readonly projectToSizeMap = new Map<string, number>(); 793 /** 794 * This is a map of config file paths existence that doesnt need query to disk 795 * - The entry can be present because there is inferred project that needs to watch addition of config file to directory 796 * In this case the exists could be true/false based on config file is present or not 797 * - Or it is present if we have configured project open with config file at that location 798 * In this case the exists property is always true 799 * 800 * @internal 801 */ 802 readonly configFileExistenceInfoCache = new Map<NormalizedPath, ConfigFileExistenceInfo>(); 803 /** @internal */ readonly throttledOperations: ThrottledOperations; 804 805 private readonly hostConfiguration: HostConfiguration; 806 private safelist: SafeList = defaultTypeSafeList; 807 private readonly legacySafelist = new Map<string, string>(); 808 809 private pendingProjectUpdates = new Map<string, Project>(); 810 /** @internal */ 811 pendingEnsureProjectForOpenFiles = false; 812 813 readonly currentDirectory: NormalizedPath; 814 readonly toCanonicalFileName: (f: string) => string; 815 816 public readonly host: ServerHost; 817 public readonly logger: Logger; 818 public readonly cancellationToken: HostCancellationToken; 819 public readonly useSingleInferredProject: boolean; 820 public readonly useInferredProjectPerProjectRoot: boolean; 821 public readonly typingsInstaller: ITypingsInstaller; 822 private readonly globalCacheLocationDirectoryPath: Path | undefined; 823 public readonly throttleWaitMilliseconds?: number; 824 private readonly eventHandler?: ProjectServiceEventHandler; 825 private readonly suppressDiagnosticEvents?: boolean; 826 827 public readonly globalPlugins: readonly string[]; 828 public readonly pluginProbeLocations: readonly string[]; 829 public readonly allowLocalPluginLoads: boolean; 830 private currentPluginConfigOverrides: ESMap<string, any> | undefined; 831 832 public readonly typesMapLocation: string | undefined; 833 834 /** @deprecated use serverMode instead */ 835 public readonly syntaxOnly: boolean; 836 public readonly serverMode: LanguageServiceMode; 837 838 /** Tracks projects that we have already sent telemetry for. */ 839 private readonly seenProjects = new Map<string, true>(); 840 841 /** @internal */ 842 readonly watchFactory: WatchFactory<WatchType, Project | NormalizedPath>; 843 844 /** @internal */ 845 private readonly sharedExtendedConfigFileWatchers = new Map<Path, SharedExtendedConfigFileWatcher<NormalizedPath>>(); 846 /** @internal */ 847 private readonly extendedConfigCache = new Map<string, ExtendedConfigCacheEntry>(); 848 849 /** @internal */ 850 readonly packageJsonCache: PackageJsonCache; 851 /** @internal */ 852 private packageJsonFilesMap: ESMap<Path, FileWatcher> | undefined; 853 /** @internal */ 854 private incompleteCompletionsCache: IncompleteCompletionsCache | undefined; 855 /** @internal */ 856 readonly session: Session<unknown> | undefined; 857 858 859 private performanceEventHandler?: PerformanceEventHandler; 860 861 private pendingPluginEnablements?: ESMap<Project, Promise<BeginEnablePluginResult>[]>; 862 private currentPluginEnablementPromise?: Promise<void>; 863 864 constructor(opts: ProjectServiceOptions) { 865 this.host = opts.host; 866 this.logger = opts.logger; 867 this.cancellationToken = opts.cancellationToken; 868 this.useSingleInferredProject = opts.useSingleInferredProject; 869 this.useInferredProjectPerProjectRoot = opts.useInferredProjectPerProjectRoot; 870 this.typingsInstaller = opts.typingsInstaller || nullTypingsInstaller; 871 this.throttleWaitMilliseconds = opts.throttleWaitMilliseconds; 872 this.eventHandler = opts.eventHandler; 873 this.suppressDiagnosticEvents = opts.suppressDiagnosticEvents; 874 this.globalPlugins = opts.globalPlugins || emptyArray; 875 this.pluginProbeLocations = opts.pluginProbeLocations || emptyArray; 876 this.allowLocalPluginLoads = !!opts.allowLocalPluginLoads; 877 this.typesMapLocation = (opts.typesMapLocation === undefined) ? combinePaths(getDirectoryPath(this.getExecutingFilePath()), "typesMap.json") : opts.typesMapLocation; 878 this.session = opts.session; 879 880 if (opts.serverMode !== undefined) { 881 this.serverMode = opts.serverMode; 882 this.syntaxOnly = this.serverMode === LanguageServiceMode.Syntactic; 883 } 884 else if (opts.syntaxOnly) { 885 this.serverMode = LanguageServiceMode.Syntactic; 886 this.syntaxOnly = true; 887 } 888 else { 889 this.serverMode = LanguageServiceMode.Semantic; 890 this.syntaxOnly = false; 891 } 892 893 if (this.host.realpath) { 894 this.realpathToScriptInfos = createMultiMap(); 895 } 896 this.currentDirectory = toNormalizedPath(this.host.getCurrentDirectory()); 897 this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); 898 this.globalCacheLocationDirectoryPath = this.typingsInstaller.globalTypingsCacheLocation 899 ? ensureTrailingDirectorySeparator(this.toPath(this.typingsInstaller.globalTypingsCacheLocation)) 900 : undefined; 901 this.throttledOperations = new ThrottledOperations(this.host, this.logger); 902 903 if (this.typesMapLocation) { 904 this.loadTypesMap(); 905 } 906 else { 907 this.logger.info("No types map provided; using the default"); 908 } 909 910 this.typingsInstaller.attach(this); 911 912 this.typingsCache = new TypingsCache(this.typingsInstaller); 913 914 this.hostConfiguration = { 915 formatCodeOptions: getDefaultFormatCodeSettings(this.host.newLine), 916 preferences: emptyOptions, 917 hostInfo: "Unknown host", 918 extraFileExtensions: [], 919 }; 920 921 this.documentRegistry = createDocumentRegistryInternal(this.host.useCaseSensitiveFileNames, this.currentDirectory, this); 922 const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : 923 this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; 924 const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; 925 this.packageJsonCache = createPackageJsonCache(this); 926 this.watchFactory = this.serverMode !== LanguageServiceMode.Semantic ? 927 { 928 watchFile: returnNoopFileWatcher, 929 watchDirectory: returnNoopFileWatcher, 930 } : 931 getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo); 932 } 933 934 toPath(fileName: string) { 935 return toPath(fileName, this.currentDirectory, this.toCanonicalFileName); 936 } 937 938 /** @internal */ 939 getExecutingFilePath() { 940 return this.getNormalizedAbsolutePath(this.host.getExecutingFilePath()); 941 } 942 943 /** @internal */ 944 getNormalizedAbsolutePath(fileName: string) { 945 return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory()); 946 } 947 948 /** @internal */ 949 setDocument(key: DocumentRegistryBucketKeyWithMode, path: Path, sourceFile: SourceFile) { 950 const info = Debug.checkDefined(this.getScriptInfoForPath(path)); 951 info.cacheSourceFile = { key, sourceFile }; 952 } 953 954 /** @internal */ 955 getDocument(key: DocumentRegistryBucketKeyWithMode, path: Path): SourceFile | undefined { 956 const info = this.getScriptInfoForPath(path); 957 return info && info.cacheSourceFile && info.cacheSourceFile.key === key ? info.cacheSourceFile.sourceFile : undefined; 958 } 959 960 /** @internal */ 961 ensureInferredProjectsUpToDate_TestOnly() { 962 this.ensureProjectStructuresUptoDate(); 963 } 964 965 /** @internal */ 966 getCompilerOptionsForInferredProjects() { 967 return this.compilerOptionsForInferredProjects; 968 } 969 970 /** @internal */ 971 onUpdateLanguageServiceStateForProject(project: Project, languageServiceEnabled: boolean) { 972 if (!this.eventHandler) { 973 return; 974 } 975 const event: ProjectLanguageServiceStateEvent = { 976 eventName: ProjectLanguageServiceStateEvent, 977 data: { project, languageServiceEnabled } 978 }; 979 this.eventHandler(event); 980 } 981 982 private loadTypesMap() { 983 try { 984 const fileContent = this.host.readFile(this.typesMapLocation!); // TODO: GH#18217 985 if (fileContent === undefined) { 986 this.logger.info(`Provided types map file "${this.typesMapLocation}" doesn't exist`); 987 return; 988 } 989 const raw: TypesMapFile = JSON.parse(fileContent); 990 // Parse the regexps 991 for (const k of Object.keys(raw.typesMap)) { 992 raw.typesMap[k].match = new RegExp(raw.typesMap[k].match as {} as string, "i"); 993 } 994 // raw is now fixed and ready 995 this.safelist = raw.typesMap; 996 for (const key in raw.simpleMap) { 997 if (hasProperty(raw.simpleMap, key)) { 998 this.legacySafelist.set(key, raw.simpleMap[key].toLowerCase()); 999 } 1000 } 1001 } 1002 catch (e) { 1003 this.logger.info(`Error loading types map: ${e}`); 1004 this.safelist = defaultTypeSafeList; 1005 this.legacySafelist.clear(); 1006 } 1007 } 1008 1009 updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void; 1010 /** @internal */ 1011 updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void; // eslint-disable-line @typescript-eslint/unified-signatures 1012 updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void { 1013 const project = this.findProject(response.projectName); 1014 if (!project) { 1015 return; 1016 } 1017 switch (response.kind) { 1018 case ActionSet: 1019 // Update the typing files and update the project 1020 project.updateTypingFiles(this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typeAcquisition, response.unresolvedImports, response.typings)); 1021 return; 1022 case ActionInvalidate: 1023 // Do not clear resolution cache, there was changes detected in typings, so enque typing request and let it get us correct results 1024 this.typingsCache.enqueueInstallTypingsForProject(project, project.lastCachedUnresolvedImportsList, /*forceRefresh*/ true); 1025 return; 1026 } 1027 } 1028 1029 /** @internal */ 1030 delayEnsureProjectForOpenFiles() { 1031 if (!this.openFiles.size) return; 1032 this.pendingEnsureProjectForOpenFiles = true; 1033 this.throttledOperations.schedule(ensureProjectForOpenFileSchedule, /*delay*/ 2500, () => { 1034 if (this.pendingProjectUpdates.size !== 0) { 1035 this.delayEnsureProjectForOpenFiles(); 1036 } 1037 else { 1038 if (this.pendingEnsureProjectForOpenFiles) { 1039 this.ensureProjectForOpenFiles(); 1040 1041 // Send the event to notify that there were background project updates 1042 // send current list of open files 1043 this.sendProjectsUpdatedInBackgroundEvent(); 1044 } 1045 } 1046 }); 1047 } 1048 1049 private delayUpdateProjectGraph(project: Project) { 1050 project.markAsDirty(); 1051 if (project.projectKind !== ProjectKind.AutoImportProvider && project.projectKind !== ProjectKind.Auxiliary) { 1052 const projectName = project.getProjectName(); 1053 this.pendingProjectUpdates.set(projectName, project); 1054 this.throttledOperations.schedule(projectName, /*delay*/ 250, () => { 1055 if (this.pendingProjectUpdates.delete(projectName)) { 1056 updateProjectIfDirty(project); 1057 } 1058 }); 1059 } 1060 } 1061 1062 /** @internal */ 1063 hasPendingProjectUpdate(project: Project) { 1064 return this.pendingProjectUpdates.has(project.getProjectName()); 1065 } 1066 1067 /** @internal */ 1068 sendProjectsUpdatedInBackgroundEvent() { 1069 if (!this.eventHandler) { 1070 return; 1071 } 1072 1073 const event: ProjectsUpdatedInBackgroundEvent = { 1074 eventName: ProjectsUpdatedInBackgroundEvent, 1075 data: { 1076 openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path)!.fileName) 1077 } 1078 }; 1079 this.eventHandler(event); 1080 } 1081 1082 /** @internal */ 1083 sendLargeFileReferencedEvent(file: string, fileSize: number) { 1084 if (!this.eventHandler) { 1085 return; 1086 } 1087 1088 const event: LargeFileReferencedEvent = { 1089 eventName: LargeFileReferencedEvent, 1090 data: { file, fileSize, maxFileSize } 1091 }; 1092 this.eventHandler(event); 1093 } 1094 1095 /** @internal */ 1096 sendProjectLoadingStartEvent(project: ConfiguredProject, reason: string) { 1097 if (!this.eventHandler) { 1098 return; 1099 } 1100 project.sendLoadingProjectFinish = true; 1101 const event: ProjectLoadingStartEvent = { 1102 eventName: ProjectLoadingStartEvent, 1103 data: { project, reason } 1104 }; 1105 this.eventHandler(event); 1106 } 1107 1108 /** @internal */ 1109 sendProjectLoadingFinishEvent(project: ConfiguredProject) { 1110 if (!this.eventHandler || !project.sendLoadingProjectFinish) { 1111 return; 1112 } 1113 1114 project.sendLoadingProjectFinish = false; 1115 const event: ProjectLoadingFinishEvent = { 1116 eventName: ProjectLoadingFinishEvent, 1117 data: { project } 1118 }; 1119 this.eventHandler(event); 1120 } 1121 1122 /** @internal */ 1123 sendPerformanceEvent(kind: PerformanceEvent["kind"], durationMs: number) { 1124 if (this.performanceEventHandler) { 1125 this.performanceEventHandler({ kind, durationMs }); 1126 } 1127 } 1128 1129 /** @internal */ 1130 delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project: Project) { 1131 this.delayUpdateProjectGraph(project); 1132 this.delayEnsureProjectForOpenFiles(); 1133 } 1134 1135 private delayUpdateProjectGraphs(projects: readonly Project[], clearSourceMapperCache: boolean) { 1136 if (projects.length) { 1137 for (const project of projects) { 1138 // Even if program doesnt change, clear the source mapper cache 1139 if (clearSourceMapperCache) project.clearSourceMapperCache(); 1140 this.delayUpdateProjectGraph(project); 1141 } 1142 this.delayEnsureProjectForOpenFiles(); 1143 } 1144 } 1145 1146 setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.InferredProjectCompilerOptions, projectRootPath?: string): void { 1147 Debug.assert(projectRootPath === undefined || this.useInferredProjectPerProjectRoot, "Setting compiler options per project root path is only supported when useInferredProjectPerProjectRoot is enabled"); 1148 1149 const compilerOptions = convertCompilerOptions(projectCompilerOptions); 1150 const watchOptions = convertWatchOptions(projectCompilerOptions, projectRootPath); 1151 const typeAcquisition = convertTypeAcquisition(projectCompilerOptions); 1152 1153 // always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside 1154 // previously we did not expose a way for user to change these settings and this option was enabled by default 1155 compilerOptions.allowNonTsExtensions = true; 1156 const canonicalProjectRootPath = projectRootPath && this.toCanonicalFileName(projectRootPath); 1157 if (canonicalProjectRootPath) { 1158 this.compilerOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, compilerOptions); 1159 this.watchOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, watchOptions || false); 1160 this.typeAcquisitionForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, typeAcquisition); 1161 } 1162 else { 1163 this.compilerOptionsForInferredProjects = compilerOptions; 1164 this.watchOptionsForInferredProjects = watchOptions; 1165 this.typeAcquisitionForInferredProjects = typeAcquisition; 1166 } 1167 1168 for (const project of this.inferredProjects) { 1169 // Only update compiler options in the following cases: 1170 // - Inferred projects without a projectRootPath, if the new options do not apply to 1171 // a workspace root 1172 // - Inferred projects with a projectRootPath, if the new options do not apply to a 1173 // workspace root and there is no more specific set of options for that project's 1174 // root path 1175 // - Inferred projects with a projectRootPath, if the new options apply to that 1176 // project root path. 1177 if (canonicalProjectRootPath ? 1178 project.projectRootPath === canonicalProjectRootPath : 1179 !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) { 1180 project.setCompilerOptions(compilerOptions); 1181 project.setTypeAcquisition(typeAcquisition); 1182 project.setWatchOptions(watchOptions?.watchOptions); 1183 project.setProjectErrors(watchOptions?.errors); 1184 project.compileOnSaveEnabled = compilerOptions.compileOnSave!; 1185 project.markAsDirty(); 1186 this.delayUpdateProjectGraph(project); 1187 } 1188 } 1189 1190 this.delayEnsureProjectForOpenFiles(); 1191 } 1192 1193 findProject(projectName: string): Project | undefined { 1194 if (projectName === undefined) { 1195 return undefined; 1196 } 1197 if (isInferredProjectName(projectName)) { 1198 return findProjectByName(projectName, this.inferredProjects); 1199 } 1200 return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); 1201 } 1202 1203 /** @internal */ 1204 private forEachProject(cb: (project: Project) => void) { 1205 this.externalProjects.forEach(cb); 1206 this.configuredProjects.forEach(cb); 1207 this.inferredProjects.forEach(cb); 1208 } 1209 1210 /** @internal */ 1211 forEachEnabledProject(cb: (project: Project) => void) { 1212 this.forEachProject(project => { 1213 if (!project.isOrphan() && project.languageServiceEnabled) { 1214 cb(project); 1215 } 1216 }); 1217 } 1218 1219 getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined { 1220 return ensureProject ? this.ensureDefaultProjectForFile(fileName) : this.tryGetDefaultProjectForFile(fileName); 1221 } 1222 1223 /** @internal */ 1224 tryGetDefaultProjectForFile(fileNameOrScriptInfo: NormalizedPath | ScriptInfo): Project | undefined { 1225 const scriptInfo = isString(fileNameOrScriptInfo) ? this.getScriptInfoForNormalizedPath(fileNameOrScriptInfo) : fileNameOrScriptInfo; 1226 return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : undefined; 1227 } 1228 1229 /** @internal */ 1230 ensureDefaultProjectForFile(fileNameOrScriptInfo: NormalizedPath | ScriptInfo): Project { 1231 return this.tryGetDefaultProjectForFile(fileNameOrScriptInfo) || this.doEnsureDefaultProjectForFile(fileNameOrScriptInfo); 1232 } 1233 1234 private doEnsureDefaultProjectForFile(fileNameOrScriptInfo: NormalizedPath | ScriptInfo): Project { 1235 this.ensureProjectStructuresUptoDate(); 1236 const scriptInfo = isString(fileNameOrScriptInfo) ? this.getScriptInfoForNormalizedPath(fileNameOrScriptInfo) : fileNameOrScriptInfo; 1237 return scriptInfo ? 1238 scriptInfo.getDefaultProject() : 1239 (this.logErrorForScriptInfoNotFound(isString(fileNameOrScriptInfo) ? fileNameOrScriptInfo : fileNameOrScriptInfo.fileName), Errors.ThrowNoProject()); 1240 } 1241 1242 getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) { 1243 this.ensureProjectStructuresUptoDate(); 1244 return this.getScriptInfo(uncheckedFileName); 1245 } 1246 1247 /** 1248 * Ensures the project structures are upto date 1249 * This means, 1250 * - we go through all the projects and update them if they are dirty 1251 * - if updates reflect some change in structure or there was pending request to ensure projects for open files 1252 * ensure that each open script info has project 1253 */ 1254 private ensureProjectStructuresUptoDate() { 1255 let hasChanges = this.pendingEnsureProjectForOpenFiles; 1256 this.pendingProjectUpdates.clear(); 1257 const updateGraph = (project: Project) => { 1258 hasChanges = updateProjectIfDirty(project) || hasChanges; 1259 }; 1260 1261 this.externalProjects.forEach(updateGraph); 1262 this.configuredProjects.forEach(updateGraph); 1263 this.inferredProjects.forEach(updateGraph); 1264 if (hasChanges) { 1265 this.ensureProjectForOpenFiles(); 1266 } 1267 } 1268 1269 getFormatCodeOptions(file: NormalizedPath) { 1270 const info = this.getScriptInfoForNormalizedPath(file); 1271 return info && info.getFormatCodeSettings() || this.hostConfiguration.formatCodeOptions; 1272 } 1273 1274 getPreferences(file: NormalizedPath): protocol.UserPreferences { 1275 const info = this.getScriptInfoForNormalizedPath(file); 1276 return { ...this.hostConfiguration.preferences, ...info && info.getPreferences() }; 1277 } 1278 1279 getHostFormatCodeOptions(): FormatCodeSettings { 1280 return this.hostConfiguration.formatCodeOptions; 1281 } 1282 1283 getHostPreferences(): protocol.UserPreferences { 1284 return this.hostConfiguration.preferences; 1285 } 1286 1287 private onSourceFileChanged(info: ScriptInfo, eventKind: FileWatcherEventKind) { 1288 if (eventKind === FileWatcherEventKind.Deleted) { 1289 // File was deleted 1290 this.handleDeletedFile(info); 1291 } 1292 else if (!info.isScriptOpen()) { 1293 // file has been changed which might affect the set of referenced files in projects that include 1294 // this file and set of inferred projects 1295 info.delayReloadNonMixedContentFile(); 1296 this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ false); 1297 this.handleSourceMapProjects(info); 1298 } 1299 } 1300 1301 private handleSourceMapProjects(info: ScriptInfo) { 1302 // Change in d.ts, update source projects as well 1303 if (info.sourceMapFilePath) { 1304 if (isString(info.sourceMapFilePath)) { 1305 const sourceMapFileInfo = this.getScriptInfoForPath(info.sourceMapFilePath); 1306 this.delayUpdateSourceInfoProjects(sourceMapFileInfo && sourceMapFileInfo.sourceInfos); 1307 } 1308 else { 1309 this.delayUpdateSourceInfoProjects(info.sourceMapFilePath.sourceInfos); 1310 } 1311 } 1312 // Change in mapInfo, update declarationProjects and source projects 1313 this.delayUpdateSourceInfoProjects(info.sourceInfos); 1314 if (info.declarationInfoPath) { 1315 this.delayUpdateProjectsOfScriptInfoPath(info.declarationInfoPath); 1316 } 1317 } 1318 1319 private delayUpdateSourceInfoProjects(sourceInfos: Set<Path> | undefined) { 1320 if (sourceInfos) { 1321 sourceInfos.forEach((_value, path) => this.delayUpdateProjectsOfScriptInfoPath(path)); 1322 } 1323 } 1324 1325 private delayUpdateProjectsOfScriptInfoPath(path: Path) { 1326 const info = this.getScriptInfoForPath(path); 1327 if (info) { 1328 this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ true); 1329 } 1330 } 1331 1332 private handleDeletedFile(info: ScriptInfo) { 1333 this.stopWatchingScriptInfo(info); 1334 1335 if (!info.isScriptOpen()) { 1336 this.deleteScriptInfo(info); 1337 1338 // capture list of projects since detachAllProjects will wipe out original list 1339 const containingProjects = info.containingProjects.slice(); 1340 1341 info.detachAllProjects(); 1342 1343 // update projects to make sure that set of referenced files is correct 1344 this.delayUpdateProjectGraphs(containingProjects, /*clearSourceMapperCache*/ false); 1345 this.handleSourceMapProjects(info); 1346 info.closeSourceMapFileWatcher(); 1347 // need to recalculate source map from declaration file 1348 if (info.declarationInfoPath) { 1349 const declarationInfo = this.getScriptInfoForPath(info.declarationInfoPath); 1350 if (declarationInfo) { 1351 declarationInfo.sourceMapFilePath = undefined; 1352 } 1353 } 1354 } 1355 } 1356 1357 /** 1358 * This is to watch whenever files are added or removed to the wildcard directories 1359 * 1360 * @internal 1361 */ 1362 private watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, configFileName: NormalizedPath, config: ParsedConfig) { 1363 return this.watchFactory.watchDirectory( 1364 directory, 1365 fileOrDirectory => { 1366 const fileOrDirectoryPath = this.toPath(fileOrDirectory); 1367 const fsResult = config.cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); 1368 if (getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) && 1369 (fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectoryPath)) 1370 ) { 1371 this.logger.info(`Config: ${configFileName} Detected new package.json: ${fileOrDirectory}`); 1372 this.onAddPackageJson(fileOrDirectoryPath); 1373 } 1374 1375 const configuredProjectForConfig = this.findConfiguredProjectByProjectName(configFileName); 1376 if (isIgnoredFileFromWildCardWatching({ 1377 watchedDirPath: directory, 1378 fileOrDirectory, 1379 fileOrDirectoryPath, 1380 configFileName, 1381 extraFileExtensions: this.hostConfiguration.extraFileExtensions, 1382 currentDirectory: this.currentDirectory, 1383 options: config.parsedCommandLine!.options, 1384 program: configuredProjectForConfig?.getCurrentProgram() || config.parsedCommandLine!.fileNames, 1385 useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames, 1386 writeLog: s => this.logger.info(s), 1387 toPath: s => this.toPath(s) 1388 })) return; 1389 1390 // Reload is pending, do the reload 1391 if (config.reloadLevel !== ConfigFileProgramReloadLevel.Full) config.reloadLevel = ConfigFileProgramReloadLevel.Partial; 1392 config.projects.forEach((watchWildcardDirectories, projectCanonicalPath) => { 1393 if (!watchWildcardDirectories) return; 1394 const project = this.getConfiguredProjectByCanonicalConfigFilePath(projectCanonicalPath); 1395 if (!project) return; 1396 1397 // Load root file names for configured project with the config file name 1398 // But only schedule update if project references this config file 1399 const reloadLevel = configuredProjectForConfig === project ? ConfigFileProgramReloadLevel.Partial : ConfigFileProgramReloadLevel.None; 1400 if (project.pendingReload !== undefined && project.pendingReload > reloadLevel) return; 1401 1402 // don't trigger callback on open, existing files 1403 if (this.openFiles.has(fileOrDirectoryPath)) { 1404 const info = Debug.checkDefined(this.getScriptInfoForPath(fileOrDirectoryPath)); 1405 if (info.isAttached(project)) { 1406 const loadLevelToSet = Math.max(reloadLevel, project.openFileWatchTriggered.get(fileOrDirectoryPath) || ConfigFileProgramReloadLevel.None) as ConfigFileProgramReloadLevel; 1407 project.openFileWatchTriggered.set(fileOrDirectoryPath, loadLevelToSet); 1408 } 1409 else { 1410 project.pendingReload = reloadLevel; 1411 this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project); 1412 } 1413 } 1414 else { 1415 project.pendingReload = reloadLevel; 1416 this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project); 1417 } 1418 }); 1419 }, 1420 flags, 1421 this.getWatchOptionsFromProjectWatchOptions(config.parsedCommandLine!.watchOptions), 1422 WatchType.WildcardDirectory, 1423 configFileName 1424 ); 1425 } 1426 1427 /** @internal */ 1428 private delayUpdateProjectsFromParsedConfigOnConfigFileChange(canonicalConfigFilePath: NormalizedPath, reloadReason: string) { 1429 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1430 if (!configFileExistenceInfo?.config) return false; 1431 let scheduledAnyProjectUpdate = false; 1432 // Update projects watching cached config 1433 configFileExistenceInfo.config.reloadLevel = ConfigFileProgramReloadLevel.Full; 1434 1435 configFileExistenceInfo.config.projects.forEach((_watchWildcardDirectories, projectCanonicalPath) => { 1436 const project = this.getConfiguredProjectByCanonicalConfigFilePath(projectCanonicalPath); 1437 if (!project) return; 1438 1439 scheduledAnyProjectUpdate = true; 1440 if (projectCanonicalPath === canonicalConfigFilePath) { 1441 // Skip refresh if project is not yet loaded 1442 if (project.isInitialLoadPending()) return; 1443 project.pendingReload = ConfigFileProgramReloadLevel.Full; 1444 project.pendingReloadReason = reloadReason; 1445 this.delayUpdateProjectGraph(project); 1446 } 1447 else { 1448 // Change in referenced project config file 1449 project.resolutionCache.removeResolutionsFromProjectReferenceRedirects(this.toPath(canonicalConfigFilePath)); 1450 this.delayUpdateProjectGraph(project); 1451 } 1452 }); 1453 return scheduledAnyProjectUpdate; 1454 } 1455 1456 /** @internal */ 1457 private onConfigFileChanged(canonicalConfigFilePath: NormalizedPath, eventKind: FileWatcherEventKind) { 1458 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath)!; 1459 if (eventKind === FileWatcherEventKind.Deleted) { 1460 // Update the cached status 1461 // We arent updating or removing the cached config file presence info as that will be taken care of by 1462 // releaseParsedConfig when the project is closed or doesnt need this config any more (depending on tracking open files) 1463 configFileExistenceInfo.exists = false; 1464 1465 // Remove the configured project for this config file 1466 const project = configFileExistenceInfo.config?.projects.has(canonicalConfigFilePath) ? 1467 this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath) : 1468 undefined; 1469 if (project) this.removeProject(project); 1470 } 1471 else { 1472 // Update the cached status 1473 configFileExistenceInfo.exists = true; 1474 } 1475 1476 // Update projects watching config 1477 this.delayUpdateProjectsFromParsedConfigOnConfigFileChange(canonicalConfigFilePath, "Change in config file detected"); 1478 1479 // Reload the configured projects for the open files in the map as they are affected by this config file 1480 // If the configured project was deleted, we want to reload projects for all the open files including files 1481 // that are not root of the inferred project 1482 // Otherwise, we scheduled the update on configured project graph, 1483 // we would need to schedule the project reload for only the root of inferred projects 1484 // Get open files to reload projects for 1485 this.reloadConfiguredProjectForFiles( 1486 configFileExistenceInfo.openFilesImpactedByConfigFile, 1487 /*clearSemanticCache*/ false, 1488 /*delayReload*/ true, 1489 eventKind !== FileWatcherEventKind.Deleted ? 1490 identity : // Reload open files if they are root of inferred project 1491 returnTrue, // Reload all the open files impacted by config file 1492 "Change in config file detected" 1493 ); 1494 this.delayEnsureProjectForOpenFiles(); 1495 } 1496 1497 private removeProject(project: Project) { 1498 this.logger.info("`remove Project::"); 1499 project.print(/*writeProjectFileNames*/ true); 1500 1501 project.close(); 1502 if (Debug.shouldAssert(AssertionLevel.Normal)) { 1503 this.filenameToScriptInfo.forEach(info => Debug.assert( 1504 !info.isAttached(project), 1505 "Found script Info still attached to project", 1506 () => `${project.projectName}: ScriptInfos still attached: ${JSON.stringify( 1507 arrayFrom( 1508 mapDefinedIterator( 1509 this.filenameToScriptInfo.values(), 1510 info => info.isAttached(project) ? 1511 { 1512 fileName: info.fileName, 1513 projects: info.containingProjects.map(p => p.projectName), 1514 hasMixedContent: info.hasMixedContent 1515 } : undefined 1516 ) 1517 ), 1518 /*replacer*/ undefined, 1519 " " 1520 )}`)); 1521 } 1522 // Remove the project from pending project updates 1523 this.pendingProjectUpdates.delete(project.getProjectName()); 1524 1525 switch (project.projectKind) { 1526 case ProjectKind.External: 1527 unorderedRemoveItem(this.externalProjects, project as ExternalProject); 1528 this.projectToSizeMap.delete(project.getProjectName()); 1529 break; 1530 case ProjectKind.Configured: 1531 this.configuredProjects.delete((project as ConfiguredProject).canonicalConfigFilePath); 1532 this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); 1533 break; 1534 case ProjectKind.Inferred: 1535 unorderedRemoveItem(this.inferredProjects, project as InferredProject); 1536 break; 1537 } 1538 } 1539 1540 /** @internal */ 1541 assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) { 1542 Debug.assert(info.isOrphan()); 1543 1544 const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) || 1545 this.getOrCreateSingleInferredProjectIfEnabled() || 1546 this.getOrCreateSingleInferredWithoutProjectRoot( 1547 info.isDynamic ? 1548 projectRootPath || this.currentDirectory : 1549 getDirectoryPath( 1550 isRootedDiskPath(info.fileName) ? 1551 info.fileName : 1552 getNormalizedAbsolutePath( 1553 info.fileName, 1554 projectRootPath ? 1555 this.getNormalizedAbsolutePath(projectRootPath) : 1556 this.currentDirectory 1557 ) 1558 ) 1559 ); 1560 1561 project.addRoot(info); 1562 if (info.containingProjects[0] !== project) { 1563 // Ensure this is first project, we could be in this scenario because info could be part of orphan project 1564 info.detachFromProject(project); 1565 info.containingProjects.unshift(project); 1566 } 1567 project.updateGraph(); 1568 1569 if (!this.useSingleInferredProject && !project.projectRootPath) { 1570 // Note that we need to create a copy of the array since the list of project can change 1571 for (const inferredProject of this.inferredProjects) { 1572 if (inferredProject === project || inferredProject.isOrphan()) { 1573 continue; 1574 } 1575 1576 // Remove the inferred project if the root of it is now part of newly created inferred project 1577 // e.g through references 1578 // Which means if any root of inferred project is part of more than 1 project can be removed 1579 // This logic is same as iterating over all open files and calling 1580 // this.removeRootOfInferredProjectIfNowPartOfOtherProject(f); 1581 // Since this is also called from refreshInferredProject and closeOpen file 1582 // to update inferred projects of the open file, this iteration might be faster 1583 // instead of scanning all open files 1584 const roots = inferredProject.getRootScriptInfos(); 1585 Debug.assert(roots.length === 1 || !!inferredProject.projectRootPath); 1586 if (roots.length === 1 && forEach(roots[0].containingProjects, p => p !== roots[0].containingProjects[0] && !p.isOrphan())) { 1587 inferredProject.removeFile(roots[0], /*fileExists*/ true, /*detachFromProject*/ true); 1588 } 1589 } 1590 } 1591 1592 return project; 1593 } 1594 1595 private assignOrphanScriptInfosToInferredProject() { 1596 // collect orphaned files and assign them to inferred project just like we treat open of a file 1597 this.openFiles.forEach((projectRootPath, path) => { 1598 const info = this.getScriptInfoForPath(path as Path)!; 1599 // collect all orphaned script infos from open files 1600 if (info.isOrphan()) { 1601 this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); 1602 } 1603 }); 1604 } 1605 1606 /** 1607 * Remove this file from the set of open, non-configured files. 1608 * @param info The file that has been closed or newly configured 1609 */ 1610 private closeOpenFile(info: ScriptInfo, skipAssignOrphanScriptInfosToInferredProject?: true) { 1611 // Closing file should trigger re-reading the file content from disk. This is 1612 // because the user may chose to discard the buffer content before saving 1613 // to the disk, and the server's version of the file can be out of sync. 1614 const fileExists = info.isDynamic ? false : this.host.fileExists(info.fileName); 1615 info.close(fileExists); 1616 this.stopWatchingConfigFilesForClosedScriptInfo(info); 1617 1618 const canonicalFileName = this.toCanonicalFileName(info.fileName); 1619 if (this.openFilesWithNonRootedDiskPath.get(canonicalFileName) === info) { 1620 this.openFilesWithNonRootedDiskPath.delete(canonicalFileName); 1621 } 1622 1623 // collect all projects that should be removed 1624 let ensureProjectsForOpenFiles = false; 1625 for (const p of info.containingProjects) { 1626 if (isConfiguredProject(p)) { 1627 if (info.hasMixedContent) { 1628 info.registerFileUpdate(); 1629 } 1630 // Do not remove the project so that we can reuse this project 1631 // if it would need to be re-created with next file open 1632 1633 // If project had open file affecting 1634 // Reload the root Files from config if its not already scheduled 1635 const reloadLevel = p.openFileWatchTriggered.get(info.path); 1636 if (reloadLevel !== undefined) { 1637 p.openFileWatchTriggered.delete(info.path); 1638 if (p.pendingReload !== undefined && p.pendingReload < reloadLevel) { 1639 p.pendingReload = reloadLevel; 1640 p.markFileAsDirty(info.path); 1641 } 1642 } 1643 } 1644 else if (isInferredProject(p) && p.isRoot(info)) { 1645 // If this was the last open root file of inferred project 1646 if (p.isProjectWithSingleRoot()) { 1647 ensureProjectsForOpenFiles = true; 1648 } 1649 1650 p.removeFile(info, fileExists, /*detachFromProject*/ true); 1651 // Do not remove the project even if this was last root of the inferred project 1652 // so that we can reuse this project, if it would need to be re-created with next file open 1653 } 1654 1655 if (!p.languageServiceEnabled) { 1656 // if project language service is disabled then we create a program only for open files. 1657 // this means that project should be marked as dirty to force rebuilding of the program 1658 // on the next request 1659 p.markAsDirty(); 1660 } 1661 } 1662 1663 this.openFiles.delete(info.path); 1664 this.configFileForOpenFiles.delete(info.path); 1665 1666 if (!skipAssignOrphanScriptInfosToInferredProject && ensureProjectsForOpenFiles) { 1667 this.assignOrphanScriptInfosToInferredProject(); 1668 } 1669 1670 // Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project) 1671 // is postponed to next file open so that if file from same project is opened, 1672 // we wont end up creating same script infos 1673 1674 // If the current info is being just closed - add the watcher file to track changes 1675 // But if file was deleted, handle that part 1676 if (fileExists) { 1677 this.watchClosedScriptInfo(info); 1678 } 1679 else { 1680 this.handleDeletedFile(info); 1681 } 1682 1683 return ensureProjectsForOpenFiles; 1684 } 1685 1686 private deleteScriptInfo(info: ScriptInfo) { 1687 this.filenameToScriptInfo.delete(info.path); 1688 this.filenameToScriptInfoVersion.set(info.path, info.getVersion()); 1689 const realpath = info.getRealpathIfDifferent(); 1690 if (realpath) { 1691 this.realpathToScriptInfos!.remove(realpath, info); // TODO: GH#18217 1692 } 1693 } 1694 1695 private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: NormalizedPath, info: OpenScriptInfoOrClosedOrConfigFileInfo) { 1696 let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1697 if (configFileExistenceInfo) { 1698 // By default the info would get impacted by presence of config file since its in the detection path 1699 // Only adding the info as a root to inferred project will need the existence to be watched by file watcher 1700 if (isOpenScriptInfo(info) && !configFileExistenceInfo.openFilesImpactedByConfigFile?.has(info.path)) { 1701 (configFileExistenceInfo.openFilesImpactedByConfigFile ||= new Map()).set(info.path, false); 1702 } 1703 return configFileExistenceInfo.exists; 1704 } 1705 1706 // Theoretically we should be adding watch for the directory here itself. 1707 // In practice there will be very few scenarios where the config file gets added 1708 // somewhere inside the another config file directory. 1709 // And technically we could handle that case in configFile's directory watcher in some cases 1710 // But given that its a rare scenario it seems like too much overhead. (we werent watching those directories earlier either) 1711 1712 // So what we are now watching is: configFile if the configured project corresponding to it is open 1713 // Or the whole chain of config files for the roots of the inferred projects 1714 1715 // Cache the host value of file exists and add the info to map of open files impacted by this config file 1716 const exists = this.host.fileExists(configFileName); 1717 let openFilesImpactedByConfigFile: ESMap<Path, boolean> | undefined; 1718 if (isOpenScriptInfo(info)) { 1719 (openFilesImpactedByConfigFile ||= new Map()).set(info.path, false); 1720 } 1721 configFileExistenceInfo = { exists, openFilesImpactedByConfigFile }; 1722 this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); 1723 return exists; 1724 } 1725 1726 /** @internal */ 1727 private createConfigFileWatcherForParsedConfig(configFileName: NormalizedPath, canonicalConfigFilePath: NormalizedPath, forProject: ConfiguredProject) { 1728 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath)!; 1729 // When watching config file for parsed config, remove the noopFileWatcher that can be created for open files impacted by config file and watch for real 1730 if (!configFileExistenceInfo.watcher || configFileExistenceInfo.watcher === noopConfigFileWatcher) { 1731 configFileExistenceInfo.watcher = this.watchFactory.watchFile( 1732 configFileName, 1733 (_fileName, eventKind) => this.onConfigFileChanged(canonicalConfigFilePath, eventKind), 1734 PollingInterval.High, 1735 this.getWatchOptionsFromProjectWatchOptions(configFileExistenceInfo?.config?.parsedCommandLine?.watchOptions), 1736 WatchType.ConfigFile, 1737 forProject 1738 ); 1739 } 1740 // Watching config file for project, update the map 1741 const projects = configFileExistenceInfo.config!.projects; 1742 projects.set(forProject.canonicalConfigFilePath, projects.get(forProject.canonicalConfigFilePath) || false); 1743 } 1744 1745 /** 1746 * Returns true if the configFileExistenceInfo is needed/impacted by open files that are root of inferred project 1747 */ 1748 private configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo: ConfigFileExistenceInfo) { 1749 return configFileExistenceInfo.openFilesImpactedByConfigFile && 1750 forEachEntry(configFileExistenceInfo.openFilesImpactedByConfigFile, identity); 1751 } 1752 1753 /** @internal */ 1754 releaseParsedConfig(canonicalConfigFilePath: NormalizedPath, forProject: ConfiguredProject) { 1755 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath)!; 1756 if (!configFileExistenceInfo.config?.projects.delete(forProject.canonicalConfigFilePath)) return; 1757 // If there are still projects watching this config file existence and config, there is nothing to do 1758 if (configFileExistenceInfo.config?.projects.size) return; 1759 1760 configFileExistenceInfo.config = undefined; 1761 clearSharedExtendedConfigFileWatcher(canonicalConfigFilePath, this.sharedExtendedConfigFileWatchers); 1762 Debug.checkDefined(configFileExistenceInfo.watcher); 1763 if (configFileExistenceInfo.openFilesImpactedByConfigFile?.size) { 1764 // If there are open files that are impacted by this config file existence 1765 // but none of them are root of inferred project, the config file watcher will be 1766 // created when any of the script infos are added as root of inferred project 1767 if (this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { 1768 // If we cannot watch config file existence without configured project, close the configured file watcher 1769 if (!canWatchDirectoryOrFile(getDirectoryPath(canonicalConfigFilePath) as Path)) { 1770 configFileExistenceInfo.watcher!.close(); 1771 configFileExistenceInfo.watcher = noopConfigFileWatcher; 1772 } 1773 } 1774 else { 1775 // Close existing watcher 1776 configFileExistenceInfo.watcher!.close(); 1777 configFileExistenceInfo.watcher = undefined; 1778 } 1779 } 1780 else { 1781 // There is not a single file open thats tracking the status of this config file. Remove from cache 1782 configFileExistenceInfo.watcher!.close(); 1783 this.configFileExistenceInfoCache.delete(canonicalConfigFilePath); 1784 } 1785 } 1786 1787 /** 1788 * Close the config file watcher in the cached ConfigFileExistenceInfo 1789 * if there arent any open files that are root of inferred project and there is no parsed config held by any project 1790 * 1791 * @internal 1792 */ 1793 private closeConfigFileWatcherOnReleaseOfOpenFile(configFileExistenceInfo: ConfigFileExistenceInfo) { 1794 // Close the config file watcher if there are no more open files that are root of inferred project 1795 // or if there are no projects that need to watch this config file existence info 1796 if (configFileExistenceInfo.watcher && 1797 !configFileExistenceInfo.config && 1798 !this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { 1799 configFileExistenceInfo.watcher.close(); 1800 configFileExistenceInfo.watcher = undefined; 1801 } 1802 } 1803 1804 /** 1805 * This is called on file close, so that we stop watching the config file for this script info 1806 */ 1807 private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) { 1808 Debug.assert(!info.isScriptOpen()); 1809 this.forEachConfigFileLocation(info, canonicalConfigFilePath => { 1810 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1811 if (configFileExistenceInfo) { 1812 const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile?.get(info.path); 1813 1814 // Delete the info from map, since this file is no more open 1815 configFileExistenceInfo.openFilesImpactedByConfigFile?.delete(info.path); 1816 1817 // If the script info was not root of inferred project, 1818 // there wont be config file watch open because of this script info 1819 if (infoIsRootOfInferredProject) { 1820 // But if it is a root, it could be the last script info that is root of inferred project 1821 // and hence we would need to close the config file watcher 1822 this.closeConfigFileWatcherOnReleaseOfOpenFile(configFileExistenceInfo); 1823 } 1824 1825 // If there are no open files that are impacted by configFileExistenceInfo after closing this script info 1826 // and there is are no projects that need the config file existence or parsed config, 1827 // remove the cached existence info 1828 if (!configFileExistenceInfo.openFilesImpactedByConfigFile?.size && 1829 !configFileExistenceInfo.config) { 1830 Debug.assert(!configFileExistenceInfo.watcher); 1831 this.configFileExistenceInfoCache.delete(canonicalConfigFilePath); 1832 } 1833 } 1834 }); 1835 } 1836 1837 /** 1838 * This is called by inferred project whenever script info is added as a root 1839 * 1840 * @internal 1841 */ 1842 startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { 1843 Debug.assert(info.isScriptOpen()); 1844 this.forEachConfigFileLocation(info, (canonicalConfigFilePath, configFileName) => { 1845 let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1846 if (!configFileExistenceInfo) { 1847 // Create the cache 1848 configFileExistenceInfo = { exists: this.host.fileExists(configFileName) }; 1849 this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); 1850 } 1851 1852 // Set this file as the root of inferred project 1853 (configFileExistenceInfo.openFilesImpactedByConfigFile ||= new Map()).set(info.path, true); 1854 1855 // If there is no configured project for this config file, add the file watcher 1856 configFileExistenceInfo.watcher ||= canWatchDirectoryOrFile(getDirectoryPath(canonicalConfigFilePath) as Path) ? 1857 this.watchFactory.watchFile( 1858 configFileName, 1859 (_filename, eventKind) => this.onConfigFileChanged(canonicalConfigFilePath, eventKind), 1860 PollingInterval.High, 1861 this.hostConfiguration.watchOptions, 1862 WatchType.ConfigFileForInferredRoot 1863 ) : 1864 noopConfigFileWatcher; 1865 }); 1866 } 1867 1868 /** 1869 * This is called by inferred project whenever root script info is removed from it 1870 * 1871 * @internal 1872 */ 1873 stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { 1874 this.forEachConfigFileLocation(info, canonicalConfigFilePath => { 1875 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 1876 if (configFileExistenceInfo?.openFilesImpactedByConfigFile?.has(info.path)) { 1877 Debug.assert(info.isScriptOpen()); 1878 1879 // Info is not root of inferred project any more 1880 configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false); 1881 1882 // Close the config file watcher 1883 this.closeConfigFileWatcherOnReleaseOfOpenFile(configFileExistenceInfo); 1884 } 1885 }); 1886 } 1887 1888 /** 1889 * This function tries to search for a tsconfig.json for the given file. 1890 * This is different from the method the compiler uses because 1891 * the compiler can assume it will always start searching in the 1892 * current directory (the directory in which tsc was invoked). 1893 * The server must start searching from the directory containing 1894 * the newly opened file. 1895 */ 1896 private forEachConfigFileLocation(info: OpenScriptInfoOrClosedOrConfigFileInfo, action: (canonicalConfigFilePath: NormalizedPath, configFileName: NormalizedPath) => boolean | void) { 1897 if (this.serverMode !== LanguageServiceMode.Semantic) { 1898 return undefined; 1899 } 1900 1901 Debug.assert(!isOpenScriptInfo(info) || this.openFiles.has(info.path)); 1902 const projectRootPath = this.openFiles.get(info.path); 1903 const scriptInfo = Debug.checkDefined(this.getScriptInfo(info.path)); 1904 if (scriptInfo.isDynamic) return undefined; 1905 1906 let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); 1907 const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames); 1908 1909 // If projectRootPath doesn't contain info.path, then do normal search for config file 1910 const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot(); 1911 // For ancestor of config file always ignore its own directory since its going to result in itself 1912 let searchInDirectory = !isAncestorConfigFileInfo(info); 1913 do { 1914 if (searchInDirectory) { 1915 const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName); 1916 const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); 1917 let result = action(combinePaths(canonicalSearchPath, "tsconfig.json") as NormalizedPath, tsconfigFileName); 1918 if (result) return tsconfigFileName; 1919 1920 const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); 1921 result = action(combinePaths(canonicalSearchPath, "jsconfig.json") as NormalizedPath, jsconfigFileName); 1922 if (result) return jsconfigFileName; 1923 1924 // If we started within node_modules, don't look outside node_modules. 1925 // Otherwise, we might pick up a very large project and pull in the world, 1926 // causing an editor delay. 1927 if (isNodeModulesDirectory(canonicalSearchPath)) { 1928 break; 1929 } 1930 } 1931 1932 const parentPath = asNormalizedPath(getDirectoryPath(searchPath)); 1933 if (parentPath === searchPath) break; 1934 searchPath = parentPath; 1935 searchInDirectory = true; 1936 } while (anySearchPathOk || isSearchPathInProjectRoot()); 1937 1938 return undefined; 1939 } 1940 1941 /** @internal */ 1942 findDefaultConfiguredProject(info: ScriptInfo) { 1943 if (!info.isScriptOpen()) return undefined; 1944 const configFileName = this.getConfigFileNameForFile(info); 1945 const project = configFileName && 1946 this.findConfiguredProjectByProjectName(configFileName); 1947 1948 return project && projectContainsInfoDirectly(project, info) ? 1949 project : 1950 project?.getDefaultChildProjectFromProjectWithReferences(info); 1951 } 1952 1953 /** 1954 * This function tries to search for a tsconfig.json for the given file. 1955 * This is different from the method the compiler uses because 1956 * the compiler can assume it will always start searching in the 1957 * current directory (the directory in which tsc was invoked). 1958 * The server must start searching from the directory containing 1959 * the newly opened file. 1960 * If script info is passed in, it is asserted to be open script info 1961 * otherwise just file name 1962 */ 1963 private getConfigFileNameForFile(info: OpenScriptInfoOrClosedOrConfigFileInfo) { 1964 if (isOpenScriptInfo(info)) { 1965 Debug.assert(info.isScriptOpen()); 1966 const result = this.configFileForOpenFiles.get(info.path); 1967 if (result !== undefined) return result || undefined; 1968 } 1969 this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); 1970 const configFileName = this.forEachConfigFileLocation(info, (canonicalConfigFilePath, configFileName) => 1971 this.configFileExists(configFileName, canonicalConfigFilePath, info)); 1972 if (configFileName) { 1973 this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`); 1974 } 1975 else { 1976 this.logger.info(`For info: ${info.fileName} :: No config files found.`); 1977 } 1978 if (isOpenScriptInfo(info)) { 1979 this.configFileForOpenFiles.set(info.path, configFileName || false); 1980 } 1981 return configFileName; 1982 } 1983 1984 private printProjects() { 1985 if (!this.logger.hasLevel(LogLevel.normal)) { 1986 return; 1987 } 1988 1989 this.logger.startGroup(); 1990 1991 this.externalProjects.forEach(printProjectWithoutFileNames); 1992 this.configuredProjects.forEach(printProjectWithoutFileNames); 1993 this.inferredProjects.forEach(printProjectWithoutFileNames); 1994 1995 this.logger.info("Open files: "); 1996 this.openFiles.forEach((projectRootPath, path) => { 1997 const info = this.getScriptInfoForPath(path as Path)!; 1998 this.logger.info(`\tFileName: ${info.fileName} ProjectRootPath: ${projectRootPath}`); 1999 this.logger.info(`\t\tProjects: ${info.containingProjects.map(p => p.getProjectName())}`); 2000 }); 2001 2002 this.logger.endGroup(); 2003 } 2004 2005 /** @internal */ 2006 findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined { 2007 // make sure that casing of config file name is consistent 2008 const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); 2009 return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath); 2010 } 2011 2012 private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined { 2013 return this.configuredProjects.get(canonicalConfigFilePath); 2014 } 2015 2016 private findExternalProjectByProjectName(projectFileName: string) { 2017 return findProjectByName(projectFileName, this.externalProjects); 2018 } 2019 2020 /** Get a filename if the language service exceeds the maximum allowed program size; otherwise returns undefined. */ 2021 private getFilenameForExceededTotalSizeLimitForNonTsFiles<T>(name: string, options: CompilerOptions | undefined, fileNames: T[], propertyReader: FilePropertyReader<T>): string | undefined { 2022 if (options && options.disableSizeLimit || !this.host.getFileSize) { 2023 return; 2024 } 2025 2026 let availableSpace = maxProgramSizeForNonTsFiles; 2027 this.projectToSizeMap.set(name, 0); 2028 this.projectToSizeMap.forEach(val => (availableSpace -= (val || 0))); 2029 2030 let totalNonTsFileSize = 0; 2031 2032 for (const f of fileNames) { 2033 const fileName = propertyReader.getFileName(f); 2034 if (hasTSFileExtension(fileName)) { 2035 continue; 2036 } 2037 2038 totalNonTsFileSize += this.host.getFileSize(fileName); 2039 2040 if (totalNonTsFileSize > maxProgramSizeForNonTsFiles || totalNonTsFileSize > availableSpace) { 2041 const top5LargestFiles = fileNames.map(f => propertyReader.getFileName(f)) 2042 .filter(name => !hasTSFileExtension(name)) 2043 .map(name => ({ name, size: this.host.getFileSize!(name) })) 2044 .sort((a, b) => b.size - a.size) 2045 .slice(0, 5); 2046 this.logger.info(`Non TS file size exceeded limit (${totalNonTsFileSize}). Largest files: ${top5LargestFiles.map(file => `${file.name}:${file.size}`).join(", ")}`); 2047 // Keep the size as zero since it's disabled 2048 return fileName; 2049 } 2050 } 2051 this.projectToSizeMap.set(name, totalNonTsFileSize); 2052 } 2053 2054 private createExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition, excludedFiles: NormalizedPath[]) { 2055 const compilerOptions = convertCompilerOptions(options); 2056 const watchOptionsAndErrors = convertWatchOptions(options, getDirectoryPath(normalizeSlashes(projectFileName))); 2057 const project = new ExternalProject( 2058 projectFileName, 2059 this, 2060 this.documentRegistry, 2061 compilerOptions, 2062 /*lastFileExceededProgramSize*/ this.getFilenameForExceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader), 2063 options.compileOnSave === undefined ? true : options.compileOnSave, 2064 /*projectFilePath*/ undefined, 2065 this.currentPluginConfigOverrides, 2066 watchOptionsAndErrors?.watchOptions 2067 ); 2068 project.setProjectErrors(watchOptionsAndErrors?.errors); 2069 project.excludedFiles = excludedFiles; 2070 2071 this.addFilesToNonInferredProject(project, files, externalFilePropertyReader, typeAcquisition); 2072 this.externalProjects.push(project); 2073 return project; 2074 } 2075 2076 /** @internal */ 2077 sendProjectTelemetry(project: ExternalProject | ConfiguredProject): void { 2078 if (this.seenProjects.has(project.projectName)) { 2079 setProjectOptionsUsed(project); 2080 return; 2081 } 2082 this.seenProjects.set(project.projectName, true); 2083 2084 if (!this.eventHandler || !this.host.createSHA256Hash) { 2085 setProjectOptionsUsed(project); 2086 return; 2087 } 2088 2089 const projectOptions = isConfiguredProject(project) ? project.projectOptions as ProjectOptions : undefined; 2090 setProjectOptionsUsed(project); 2091 const data: ProjectInfoTelemetryEventData = { 2092 projectId: this.host.createSHA256Hash(project.projectName), 2093 fileStats: countEachFileTypes(project.getScriptInfos(), /*includeSizes*/ true), 2094 compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilationSettings()), 2095 typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()), 2096 extends: projectOptions && projectOptions.configHasExtendsProperty, 2097 files: projectOptions && projectOptions.configHasFilesProperty, 2098 include: projectOptions && projectOptions.configHasIncludeProperty, 2099 exclude: projectOptions && projectOptions.configHasExcludeProperty, 2100 compileOnSave: project.compileOnSaveEnabled, 2101 configFileName: configFileName(), 2102 projectType: project instanceof ExternalProject ? "external" : "configured", 2103 languageServiceEnabled: project.languageServiceEnabled, 2104 version, 2105 }; 2106 this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); 2107 2108 function configFileName(): ProjectInfoTelemetryEventData["configFileName"] { 2109 if (!isConfiguredProject(project)) { 2110 return "other"; 2111 } 2112 2113 return getBaseConfigFileName(project.getConfigFilePath()) || "other"; 2114 } 2115 2116 function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData { 2117 return { 2118 enable, 2119 include: include !== undefined && include.length !== 0, 2120 exclude: exclude !== undefined && exclude.length !== 0, 2121 }; 2122 } 2123 } 2124 2125 private addFilesToNonInferredProject<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, typeAcquisition: TypeAcquisition): void { 2126 this.updateNonInferredProjectFiles(project, files, propertyReader); 2127 project.setTypeAcquisition(typeAcquisition); 2128 } 2129 2130 /** @internal */ 2131 createConfiguredProject(configFileName: NormalizedPath) { 2132 tracing?.instant(tracing.Phase.Session, "createConfiguredProject", { configFilePath: configFileName }); 2133 this.logger.info(`Creating configuration project ${configFileName}`); 2134 const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); 2135 let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); 2136 // We could be in this scenario if project is the configured project tracked by external project 2137 // Since that route doesnt check if the config file is present or not 2138 if (!configFileExistenceInfo) { 2139 this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo = { exists: true }); 2140 } 2141 else { 2142 configFileExistenceInfo.exists = true; 2143 } 2144 if (!configFileExistenceInfo.config) { 2145 configFileExistenceInfo.config = { 2146 cachedDirectoryStructureHost: createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!, 2147 projects: new Map(), 2148 reloadLevel: ConfigFileProgramReloadLevel.Full 2149 }; 2150 } 2151 2152 const project = new ConfiguredProject( 2153 configFileName, 2154 canonicalConfigFilePath, 2155 this, 2156 this.documentRegistry, 2157 configFileExistenceInfo.config.cachedDirectoryStructureHost); 2158 this.configuredProjects.set(canonicalConfigFilePath, project); 2159 this.createConfigFileWatcherForParsedConfig(configFileName, canonicalConfigFilePath, project); 2160 return project; 2161 } 2162 2163 /** @internal */ 2164 private createConfiguredProjectWithDelayLoad(configFileName: NormalizedPath, reason: string) { 2165 const project = this.createConfiguredProject(configFileName); 2166 project.pendingReload = ConfigFileProgramReloadLevel.Full; 2167 project.pendingReloadReason = reason; 2168 return project; 2169 } 2170 2171 /** @internal */ 2172 createAndLoadConfiguredProject(configFileName: NormalizedPath, reason: string) { 2173 const project = this.createConfiguredProject(configFileName); 2174 this.loadConfiguredProject(project, reason); 2175 return project; 2176 } 2177 2178 /** @internal */ 2179 private createLoadAndUpdateConfiguredProject(configFileName: NormalizedPath, reason: string) { 2180 const project = this.createAndLoadConfiguredProject(configFileName, reason); 2181 project.updateGraph(); 2182 return project; 2183 } 2184 2185 /** 2186 * Read the config file of the project, and update the project root file names. 2187 * 2188 * @internal 2189 */ 2190 private loadConfiguredProject(project: ConfiguredProject, reason: string) { 2191 tracing?.push(tracing.Phase.Session, "loadConfiguredProject", { configFilePath: project.canonicalConfigFilePath }); 2192 this.sendProjectLoadingStartEvent(project, reason); 2193 2194 // Read updated contents from disk 2195 const configFilename = asNormalizedPath(normalizePath(project.getConfigFilePath())); 2196 const configFileExistenceInfo = this.ensureParsedConfigUptoDate( 2197 configFilename, 2198 project.canonicalConfigFilePath, 2199 this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath)!, 2200 project 2201 ); 2202 const parsedCommandLine = configFileExistenceInfo.config!.parsedCommandLine!; 2203 Debug.assert(!!parsedCommandLine.fileNames); 2204 const compilerOptions = parsedCommandLine.options; 2205 2206 // Update the project 2207 if (!project.projectOptions) { 2208 project.projectOptions = { 2209 configHasExtendsProperty: parsedCommandLine.raw.extends !== undefined, 2210 configHasFilesProperty: parsedCommandLine.raw.files !== undefined, 2211 configHasIncludeProperty: parsedCommandLine.raw.include !== undefined, 2212 configHasExcludeProperty: parsedCommandLine.raw.exclude !== undefined 2213 }; 2214 } 2215 project.canConfigFileJsonReportNoInputFiles = canJsonReportNoInputFiles(parsedCommandLine.raw); 2216 project.setProjectErrors(parsedCommandLine.options.configFile!.parseDiagnostics); 2217 project.updateReferences(parsedCommandLine.projectReferences); 2218 const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, compilerOptions, parsedCommandLine.fileNames, fileNamePropertyReader); 2219 if (lastFileExceededProgramSize) { 2220 project.disableLanguageService(lastFileExceededProgramSize); 2221 this.configFileExistenceInfoCache.forEach((_configFileExistenceInfo, canonicalConfigFilePath) => 2222 this.stopWatchingWildCards(canonicalConfigFilePath, project)); 2223 } 2224 else { 2225 project.setCompilerOptions(compilerOptions); 2226 project.setWatchOptions(parsedCommandLine.watchOptions); 2227 project.enableLanguageService(); 2228 this.watchWildcards(configFilename, configFileExistenceInfo, project); 2229 } 2230 project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides); 2231 const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles()); 2232 this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave, parsedCommandLine.watchOptions); 2233 tracing?.pop(); 2234 } 2235 2236 /** @internal */ 2237 ensureParsedConfigUptoDate(configFilename: NormalizedPath, canonicalConfigFilePath: NormalizedPath, configFileExistenceInfo: ConfigFileExistenceInfo, forProject: ConfiguredProject): ConfigFileExistenceInfo { 2238 if (configFileExistenceInfo.config) { 2239 if (!configFileExistenceInfo.config.reloadLevel) return configFileExistenceInfo; 2240 if (configFileExistenceInfo.config.reloadLevel === ConfigFileProgramReloadLevel.Partial) { 2241 this.reloadFileNamesOfParsedConfig(configFilename, configFileExistenceInfo.config); 2242 return configFileExistenceInfo; 2243 } 2244 } 2245 2246 // Parse the config file and ensure its cached 2247 const cachedDirectoryStructureHost = configFileExistenceInfo.config?.cachedDirectoryStructureHost || 2248 createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!; 2249 2250 // Read updated contents from disk 2251 const configFileContent = tryReadFile(configFilename, fileName => this.host.readFile(fileName)); 2252 const configFile = parseJsonText(configFilename, isString(configFileContent) ? configFileContent : "") as TsConfigSourceFile; 2253 const configFileErrors = configFile.parseDiagnostics as Diagnostic[]; 2254 if (!isString(configFileContent)) configFileErrors.push(configFileContent); 2255 const parsedCommandLine = parseJsonSourceFileConfigFileContent( 2256 configFile, 2257 cachedDirectoryStructureHost, 2258 getDirectoryPath(configFilename), 2259 /*existingOptions*/ {}, 2260 configFilename, 2261 /*resolutionStack*/[], 2262 this.hostConfiguration.extraFileExtensions, 2263 this.extendedConfigCache, 2264 ); 2265 2266 if (parsedCommandLine.errors.length) { 2267 configFileErrors.push(...parsedCommandLine.errors); 2268 } 2269 2270 this.logger.info(`Config: ${configFilename} : ${JSON.stringify({ 2271 rootNames: parsedCommandLine.fileNames, 2272 options: parsedCommandLine.options, 2273 watchOptions: parsedCommandLine.watchOptions, 2274 projectReferences: parsedCommandLine.projectReferences 2275 }, /*replacer*/ undefined, " ")}`); 2276 2277 const oldCommandLine = configFileExistenceInfo.config?.parsedCommandLine; 2278 if (!configFileExistenceInfo.config) { 2279 configFileExistenceInfo.config = { parsedCommandLine, cachedDirectoryStructureHost, projects: new Map() }; 2280 } 2281 else { 2282 configFileExistenceInfo.config.parsedCommandLine = parsedCommandLine; 2283 configFileExistenceInfo.config.watchedDirectoriesStale = true; 2284 configFileExistenceInfo.config.reloadLevel = undefined; 2285 } 2286 2287 // If watch options different than older options when setting for the first time, update the config file watcher 2288 if (!oldCommandLine && !isJsonEqual( 2289 // Old options 2290 this.getWatchOptionsFromProjectWatchOptions(/*projectOptions*/ undefined), 2291 // New options 2292 this.getWatchOptionsFromProjectWatchOptions(parsedCommandLine.watchOptions) 2293 )) { 2294 // Reset the config file watcher 2295 configFileExistenceInfo.watcher?.close(); 2296 configFileExistenceInfo.watcher = undefined; 2297 } 2298 2299 // Ensure there is watcher for this config file 2300 this.createConfigFileWatcherForParsedConfig(configFilename, canonicalConfigFilePath, forProject); 2301 // Watch extended config files 2302 updateSharedExtendedConfigFileWatcher( 2303 canonicalConfigFilePath, 2304 parsedCommandLine.options, 2305 this.sharedExtendedConfigFileWatchers, 2306 (extendedConfigFileName, extendedConfigFilePath) => this.watchFactory.watchFile( 2307 extendedConfigFileName, 2308 () => { 2309 // Update extended config cache 2310 cleanExtendedConfigCache(this.extendedConfigCache, extendedConfigFilePath, fileName => this.toPath(fileName)); 2311 // Update projects 2312 let ensureProjectsForOpenFiles = false; 2313 this.sharedExtendedConfigFileWatchers.get(extendedConfigFilePath)?.projects.forEach(canonicalPath => { 2314 ensureProjectsForOpenFiles = this.delayUpdateProjectsFromParsedConfigOnConfigFileChange(canonicalPath, `Change in extended config file ${extendedConfigFileName} detected`) || ensureProjectsForOpenFiles; 2315 }); 2316 if (ensureProjectsForOpenFiles) this.delayEnsureProjectForOpenFiles(); 2317 }, 2318 PollingInterval.High, 2319 this.hostConfiguration.watchOptions, 2320 WatchType.ExtendedConfigFile, 2321 configFilename 2322 ), 2323 fileName => this.toPath(fileName), 2324 ); 2325 return configFileExistenceInfo; 2326 } 2327 2328 /** @internal */ 2329 watchWildcards(configFileName: NormalizedPath, { exists, config }: ConfigFileExistenceInfo, forProject: ConfiguredProject) { 2330 config!.projects.set(forProject.canonicalConfigFilePath, true); 2331 if (exists) { 2332 if (config!.watchedDirectories && !config!.watchedDirectoriesStale) return; 2333 config!.watchedDirectoriesStale = false; 2334 updateWatchingWildcardDirectories( 2335 config!.watchedDirectories ||= new Map(), 2336 new Map(getEntries(config!.parsedCommandLine!.wildcardDirectories!)), 2337 // Create new directory watcher 2338 (directory, flags) => this.watchWildcardDirectory(directory as Path, flags, configFileName, config!), 2339 ); 2340 } 2341 else { 2342 config!.watchedDirectoriesStale = false; 2343 if (!config!.watchedDirectories) return; 2344 clearMap(config!.watchedDirectories, closeFileWatcherOf); 2345 config!.watchedDirectories = undefined; 2346 } 2347 } 2348 2349 /** @internal */ 2350 stopWatchingWildCards(canonicalConfigFilePath: NormalizedPath, forProject: ConfiguredProject) { 2351 const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath)!; 2352 if (!configFileExistenceInfo.config || 2353 !configFileExistenceInfo.config.projects.get(forProject.canonicalConfigFilePath)) { 2354 return; 2355 } 2356 2357 configFileExistenceInfo.config.projects.set(forProject.canonicalConfigFilePath, false); 2358 // If any of the project is still watching wild cards dont close the watcher 2359 if (forEachEntry(configFileExistenceInfo.config.projects, identity)) return; 2360 2361 if (configFileExistenceInfo.config.watchedDirectories) { 2362 clearMap(configFileExistenceInfo.config.watchedDirectories, closeFileWatcherOf); 2363 configFileExistenceInfo.config.watchedDirectories = undefined; 2364 } 2365 configFileExistenceInfo.config.watchedDirectoriesStale = undefined; 2366 } 2367 2368 private updateNonInferredProjectFiles<T>(project: Project, files: T[], propertyReader: FilePropertyReader<T>) { 2369 const projectRootFilesMap = project.getRootFilesMap(); 2370 const newRootScriptInfoMap = new Map<string, true>(); 2371 2372 for (const f of files) { 2373 const newRootFile = propertyReader.getFileName(f); 2374 const fileName = toNormalizedPath(newRootFile); 2375 const isDynamic = isDynamicFileName(fileName); 2376 let path: Path; 2377 // Use the project's fileExists so that it can use caching instead of reaching to disk for the query 2378 if (!isDynamic && !project.fileExists(newRootFile)) { 2379 path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); 2380 const existingValue = projectRootFilesMap.get(path); 2381 if (existingValue) { 2382 if (existingValue.info) { 2383 project.removeFile(existingValue.info, /*fileExists*/ false, /*detachFromProject*/ true); 2384 existingValue.info = undefined; 2385 } 2386 existingValue.fileName = fileName; 2387 } 2388 else { 2389 projectRootFilesMap.set(path, { fileName }); 2390 } 2391 } 2392 else { 2393 const scriptKind = propertyReader.getScriptKind(f, this.hostConfiguration.extraFileExtensions); 2394 const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); 2395 const scriptInfo = Debug.checkDefined(this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( 2396 fileName, 2397 project.currentDirectory, 2398 scriptKind, 2399 hasMixedContent, 2400 project.directoryStructureHost 2401 )); 2402 path = scriptInfo.path; 2403 const existingValue = projectRootFilesMap.get(path); 2404 // If this script info is not already a root add it 2405 if (!existingValue || existingValue.info !== scriptInfo) { 2406 project.addRoot(scriptInfo, fileName); 2407 if (scriptInfo.isScriptOpen()) { 2408 // if file is already root in some inferred project 2409 // - remove the file from that project and delete the project if necessary 2410 this.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo); 2411 } 2412 } 2413 else { 2414 // Already root update the fileName 2415 existingValue.fileName = fileName; 2416 } 2417 } 2418 newRootScriptInfoMap.set(path, true); 2419 } 2420 2421 // project's root file map size is always going to be same or larger than new roots map 2422 // as we have already all the new files to the project 2423 if (projectRootFilesMap.size > newRootScriptInfoMap.size) { 2424 projectRootFilesMap.forEach((value, path) => { 2425 if (!newRootScriptInfoMap.has(path)) { 2426 if (value.info) { 2427 project.removeFile(value.info, project.fileExists(path), /*detachFromProject*/ true); 2428 } 2429 else { 2430 projectRootFilesMap.delete(path); 2431 } 2432 } 2433 }); 2434 } 2435 2436 // Just to ensure that even if root files dont change, the changes to the non root file are picked up, 2437 // mark the project as dirty unconditionally 2438 project.markAsDirty(); 2439 } 2440 2441 private updateRootAndOptionsOfNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean | undefined, watchOptions: WatchOptions | undefined) { 2442 project.setCompilerOptions(newOptions); 2443 project.setWatchOptions(watchOptions); 2444 // VS only set the CompileOnSaveEnabled option in the request if the option was changed recently 2445 // therefore if it is undefined, it should not be updated. 2446 if (compileOnSave !== undefined) { 2447 project.compileOnSaveEnabled = compileOnSave; 2448 } 2449 this.addFilesToNonInferredProject(project, newUncheckedFiles, propertyReader, newTypeAcquisition); 2450 } 2451 2452 /** 2453 * Reload the file names from config file specs and update the project graph 2454 * 2455 * @internal 2456 */ 2457 reloadFileNamesOfConfiguredProject(project: ConfiguredProject) { 2458 const fileNames = this.reloadFileNamesOfParsedConfig(project.getConfigFilePath(), this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath)!.config!); 2459 project.updateErrorOnNoInputFiles(fileNames); 2460 this.updateNonInferredProjectFiles(project, fileNames.concat(project.getExternalFiles()), fileNamePropertyReader); 2461 return project.updateGraph(); 2462 } 2463 2464 /** @internal */ 2465 private reloadFileNamesOfParsedConfig(configFileName: NormalizedPath, config: ParsedConfig) { 2466 if (config.reloadLevel === undefined) return config.parsedCommandLine!.fileNames; 2467 Debug.assert(config.reloadLevel === ConfigFileProgramReloadLevel.Partial); 2468 const configFileSpecs = config.parsedCommandLine!.options.configFile!.configFileSpecs!; 2469 const fileNames = getFileNamesFromConfigSpecs( 2470 configFileSpecs, 2471 getDirectoryPath(configFileName), 2472 config.parsedCommandLine!.options, 2473 config.cachedDirectoryStructureHost, 2474 this.hostConfiguration.extraFileExtensions 2475 ); 2476 config.parsedCommandLine = { ...config.parsedCommandLine!, fileNames }; 2477 return fileNames; 2478 } 2479 2480 /** @internal */ 2481 setFileNamesOfAutoImportProviderProject(project: AutoImportProviderProject, fileNames: string[]) { 2482 this.updateNonInferredProjectFiles(project, fileNames, fileNamePropertyReader); 2483 } 2484 2485 /** 2486 * Read the config file of the project again by clearing the cache and update the project graph 2487 * 2488 * @internal 2489 */ 2490 reloadConfiguredProject(project: ConfiguredProject, reason: string, isInitialLoad: boolean, clearSemanticCache: boolean) { 2491 // At this point, there is no reason to not have configFile in the host 2492 const host = project.getCachedDirectoryStructureHost(); 2493 if (clearSemanticCache) this.clearSemanticCache(project); 2494 2495 // Clear the cache since we are reloading the project from disk 2496 host.clearCache(); 2497 const configFileName = project.getConfigFilePath(); 2498 this.logger.info(`${isInitialLoad ? "Loading" : "Reloading"} configured project ${configFileName}`); 2499 2500 // Load project from the disk 2501 this.loadConfiguredProject(project, reason); 2502 project.updateGraph(); 2503 2504 this.sendConfigFileDiagEvent(project, configFileName); 2505 } 2506 2507 /** @internal */ 2508 private clearSemanticCache(project: Project) { 2509 project.resolutionCache.clear(); 2510 project.getLanguageService(/*ensureSynchronized*/ false).cleanupSemanticCache(); 2511 project.markAsDirty(); 2512 } 2513 2514 private sendConfigFileDiagEvent(project: ConfiguredProject, triggerFile: NormalizedPath) { 2515 if (!this.eventHandler || this.suppressDiagnosticEvents) { 2516 return; 2517 } 2518 const diagnostics = project.getLanguageService().getCompilerOptionsDiagnostics(); 2519 diagnostics.push(...project.getAllProjectErrors()); 2520 2521 this.eventHandler({ 2522 eventName: ConfigFileDiagEvent, 2523 data: { configFileName: project.getConfigFilePath(), diagnostics, triggerFile } 2524 } as ConfigFileDiagEvent); 2525 } 2526 2527 private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined { 2528 if (!this.useInferredProjectPerProjectRoot || 2529 // Its a dynamic info opened without project root 2530 (info.isDynamic && projectRootPath === undefined)) { 2531 return undefined; 2532 } 2533 2534 if (projectRootPath) { 2535 const canonicalProjectRootPath = this.toCanonicalFileName(projectRootPath); 2536 // if we have an explicit project root path, find (or create) the matching inferred project. 2537 for (const project of this.inferredProjects) { 2538 if (project.projectRootPath === canonicalProjectRootPath) { 2539 return project; 2540 } 2541 } 2542 return this.createInferredProject(projectRootPath, /*isSingleInferredProject*/ false, projectRootPath); 2543 } 2544 2545 // we don't have an explicit root path, so we should try to find an inferred project 2546 // that more closely contains the file. 2547 let bestMatch: InferredProject | undefined; 2548 for (const project of this.inferredProjects) { 2549 // ignore single inferred projects (handled elsewhere) 2550 if (!project.projectRootPath) continue; 2551 // ignore inferred projects that don't contain the root's path 2552 if (!containsPath(project.projectRootPath, info.path, this.host.getCurrentDirectory(), !this.host.useCaseSensitiveFileNames)) continue; 2553 // ignore inferred projects that are higher up in the project root. 2554 // TODO(rbuckton): Should we add the file as a root to these as well? 2555 if (bestMatch && bestMatch.projectRootPath!.length > project.projectRootPath.length) continue; 2556 bestMatch = project; 2557 } 2558 2559 return bestMatch; 2560 } 2561 2562 private getOrCreateSingleInferredProjectIfEnabled(): InferredProject | undefined { 2563 if (!this.useSingleInferredProject) { 2564 return undefined; 2565 } 2566 2567 // If `useInferredProjectPerProjectRoot` is not enabled, then there will only be one 2568 // inferred project for all files. If `useInferredProjectPerProjectRoot` is enabled 2569 // then we want to put all files that are not opened with a `projectRootPath` into 2570 // the same inferred project. 2571 // 2572 // To avoid the cost of searching through the array and to optimize for the case where 2573 // `useInferredProjectPerProjectRoot` is not enabled, we will always put the inferred 2574 // project for non-rooted files at the front of the array. 2575 if (this.inferredProjects.length > 0 && this.inferredProjects[0].projectRootPath === undefined) { 2576 return this.inferredProjects[0]; 2577 } 2578 2579 // Single inferred project does not have a project root and hence no current directory 2580 return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true); 2581 } 2582 2583 private getOrCreateSingleInferredWithoutProjectRoot(currentDirectory: string | undefined): InferredProject { 2584 Debug.assert(!this.useSingleInferredProject); 2585 const expectedCurrentDirectory = this.toCanonicalFileName(this.getNormalizedAbsolutePath(currentDirectory || "")); 2586 // Reuse the project with same current directory but no roots 2587 for (const inferredProject of this.inferredProjects) { 2588 if (!inferredProject.projectRootPath && 2589 inferredProject.isOrphan() && 2590 inferredProject.canonicalCurrentDirectory === expectedCurrentDirectory) { 2591 return inferredProject; 2592 } 2593 } 2594 2595 return this.createInferredProject(currentDirectory); 2596 } 2597 2598 private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject { 2599 const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects!; // TODO: GH#18217 2600 let watchOptionsAndErrors: WatchOptionsAndErrors | false | undefined; 2601 let typeAcquisition: TypeAcquisition | undefined; 2602 if (projectRootPath) { 2603 watchOptionsAndErrors = this.watchOptionsForInferredProjectsPerProjectRoot.get(projectRootPath); 2604 typeAcquisition = this.typeAcquisitionForInferredProjectsPerProjectRoot.get(projectRootPath); 2605 } 2606 if (watchOptionsAndErrors === undefined) { 2607 watchOptionsAndErrors = this.watchOptionsForInferredProjects; 2608 } 2609 if (typeAcquisition === undefined) { 2610 typeAcquisition = this.typeAcquisitionForInferredProjects; 2611 } 2612 watchOptionsAndErrors = watchOptionsAndErrors || undefined; 2613 const project = new InferredProject(this, this.documentRegistry, compilerOptions, watchOptionsAndErrors?.watchOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides, typeAcquisition); 2614 project.setProjectErrors(watchOptionsAndErrors?.errors); 2615 if (isSingleInferredProject) { 2616 this.inferredProjects.unshift(project); 2617 } 2618 else { 2619 this.inferredProjects.push(project); 2620 } 2621 return project; 2622 } 2623 2624 /** @internal */ 2625 getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, currentDirectory: string, hostToQueryFileExistsOn: DirectoryStructureHost) { 2626 return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( 2627 toNormalizedPath(uncheckedFileName), currentDirectory, /*scriptKind*/ undefined, 2628 /*hasMixedContent*/ undefined, hostToQueryFileExistsOn 2629 ); 2630 } 2631 2632 getScriptInfo(uncheckedFileName: string) { 2633 return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); 2634 } 2635 2636 /** @internal */ 2637 getScriptInfoOrConfig(uncheckedFileName: string): ScriptInfoOrConfig | undefined { 2638 const path = toNormalizedPath(uncheckedFileName); 2639 const info = this.getScriptInfoForNormalizedPath(path); 2640 if (info) return info; 2641 const configProject = this.configuredProjects.get(this.toPath(uncheckedFileName)); 2642 return configProject && configProject.getCompilerOptions().configFile; 2643 } 2644 2645 /** @internal */ 2646 logErrorForScriptInfoNotFound(fileName: string): void { 2647 const names = arrayFrom(this.filenameToScriptInfo.entries()).map(([path, scriptInfo]) => ({ path, fileName: scriptInfo.fileName })); 2648 this.logger.msg(`Could not find file ${JSON.stringify(fileName)}.\nAll files are: ${JSON.stringify(names)}`, Msg.Err); 2649 } 2650 2651 /** 2652 * Returns the projects that contain script info through SymLink 2653 * Note that this does not return projects in info.containingProjects 2654 * 2655 * @internal 2656 */ 2657 getSymlinkedProjects(info: ScriptInfo): MultiMap<Path, Project> | undefined { 2658 let projects: MultiMap<Path, Project> | undefined; 2659 if (this.realpathToScriptInfos) { 2660 const realpath = info.getRealpathIfDifferent(); 2661 if (realpath) { 2662 forEach(this.realpathToScriptInfos.get(realpath), combineProjects); 2663 } 2664 forEach(this.realpathToScriptInfos.get(info.path), combineProjects); 2665 } 2666 2667 return projects; 2668 2669 function combineProjects(toAddInfo: ScriptInfo) { 2670 if (toAddInfo !== info) { 2671 for (const project of toAddInfo.containingProjects) { 2672 // Add the projects only if they can use symLink targets and not already in the list 2673 if (project.languageServiceEnabled && 2674 !project.isOrphan() && 2675 !project.getCompilerOptions().preserveSymlinks && 2676 !info.isAttached(project)) { 2677 if (!projects) { 2678 projects = createMultiMap(); 2679 projects.add(toAddInfo.path, project); 2680 } 2681 else if (!forEachEntry(projects, (projs, path) => path === toAddInfo.path ? false : contains(projs, project))) { 2682 projects.add(toAddInfo.path, project); 2683 } 2684 } 2685 } 2686 } 2687 } 2688 } 2689 2690 private watchClosedScriptInfo(info: ScriptInfo) { 2691 Debug.assert(!info.fileWatcher); 2692 // do not watch files with mixed content - server doesn't know how to interpret it 2693 // do not watch files in the global cache location 2694 if (!info.isDynamicOrHasMixedContent() && 2695 (!this.globalCacheLocationDirectoryPath || 2696 !startsWith(info.path, this.globalCacheLocationDirectoryPath))) { 2697 const indexOfNodeModules = info.path.indexOf("/node_modules/"); 2698 if (!this.host.getModifiedTime || indexOfNodeModules === -1) { 2699 info.fileWatcher = this.watchFactory.watchFile( 2700 info.fileName, 2701 (_fileName, eventKind) => this.onSourceFileChanged(info, eventKind), 2702 PollingInterval.Medium, 2703 this.hostConfiguration.watchOptions, 2704 WatchType.ClosedScriptInfo 2705 ); 2706 } 2707 else { 2708 info.mTime = this.getModifiedTime(info); 2709 info.fileWatcher = this.watchClosedScriptInfoInNodeModules(info.path.substr(0, indexOfNodeModules) as Path); 2710 } 2711 } 2712 } 2713 2714 private createNodeModulesWatcher(dir: Path) { 2715 const watcher = this.watchFactory.watchDirectory( 2716 dir, 2717 fileOrDirectory => { 2718 const fileOrDirectoryPath = removeIgnoredPath(this.toPath(fileOrDirectory)); 2719 if (!fileOrDirectoryPath) return; 2720 2721 // Clear module specifier cache for any projects whose cache was affected by 2722 // dependency package.jsons in this node_modules directory 2723 const basename = getBaseFileName(fileOrDirectoryPath); 2724 if (result.affectedModuleSpecifierCacheProjects?.size && ( 2725 basename === "package.json" || basename === "node_modules" 2726 )) { 2727 result.affectedModuleSpecifierCacheProjects.forEach(projectName => { 2728 this.findProject(projectName)?.getModuleSpecifierCache()?.clear(); 2729 }); 2730 } 2731 2732 // Refresh closed script info after an npm install 2733 if (result.refreshScriptInfoRefCount) { 2734 if (dir === fileOrDirectoryPath) { 2735 this.refreshScriptInfosInDirectory(dir); 2736 } 2737 else { 2738 const info = this.getScriptInfoForPath(fileOrDirectoryPath); 2739 if (info) { 2740 if (isScriptInfoWatchedFromNodeModules(info)) { 2741 this.refreshScriptInfo(info); 2742 } 2743 } 2744 // Folder 2745 else if (!hasExtension(fileOrDirectoryPath)) { 2746 this.refreshScriptInfosInDirectory(fileOrDirectoryPath); 2747 } 2748 } 2749 } 2750 }, 2751 WatchDirectoryFlags.Recursive, 2752 this.hostConfiguration.watchOptions, 2753 WatchType.NodeModules 2754 ); 2755 const result: NodeModulesWatcher = { 2756 refreshScriptInfoRefCount: 0, 2757 affectedModuleSpecifierCacheProjects: undefined, 2758 close: () => { 2759 if (!result.refreshScriptInfoRefCount && !result.affectedModuleSpecifierCacheProjects?.size) { 2760 watcher.close(); 2761 this.nodeModulesWatchers.delete(dir); 2762 } 2763 }, 2764 }; 2765 this.nodeModulesWatchers.set(dir, result); 2766 return result; 2767 } 2768 2769 /** @internal */ 2770 watchPackageJsonsInNodeModules(dir: Path, project: Project): FileWatcher { 2771 const watcher = this.nodeModulesWatchers.get(dir) || this.createNodeModulesWatcher(dir); 2772 (watcher.affectedModuleSpecifierCacheProjects ||= new Set()).add(project.getProjectName()); 2773 2774 return { 2775 close: () => { 2776 watcher.affectedModuleSpecifierCacheProjects?.delete(project.getProjectName()); 2777 watcher.close(); 2778 }, 2779 }; 2780 } 2781 2782 private watchClosedScriptInfoInNodeModules(dir: Path): FileWatcher { 2783 const watchDir = dir + "/node_modules" as Path; 2784 const watcher = this.nodeModulesWatchers.get(watchDir) || this.createNodeModulesWatcher(watchDir); 2785 watcher.refreshScriptInfoRefCount++; 2786 2787 return { 2788 close: () => { 2789 watcher.refreshScriptInfoRefCount--; 2790 watcher.close(); 2791 }, 2792 }; 2793 } 2794 2795 private getModifiedTime(info: ScriptInfo) { 2796 return (this.host.getModifiedTime!(info.path) || missingFileModifiedTime).getTime(); 2797 } 2798 2799 private refreshScriptInfo(info: ScriptInfo) { 2800 const mTime = this.getModifiedTime(info); 2801 if (mTime !== info.mTime) { 2802 const eventKind = getFileWatcherEventKind(info.mTime!, mTime); 2803 info.mTime = mTime; 2804 this.onSourceFileChanged(info, eventKind); 2805 } 2806 } 2807 2808 private refreshScriptInfosInDirectory(dir: Path) { 2809 dir = dir + directorySeparator as Path; 2810 this.filenameToScriptInfo.forEach(info => { 2811 if (isScriptInfoWatchedFromNodeModules(info) && startsWith(info.path, dir)) { 2812 this.refreshScriptInfo(info); 2813 } 2814 }); 2815 } 2816 2817 private stopWatchingScriptInfo(info: ScriptInfo) { 2818 if (info.fileWatcher) { 2819 info.fileWatcher.close(); 2820 info.fileWatcher = undefined; 2821 } 2822 } 2823 2824 private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) { 2825 if (isRootedDiskPath(fileName) || isDynamicFileName(fileName)) { 2826 return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn); 2827 } 2828 2829 // This is non rooted path with different current directory than project service current directory 2830 // Only paths recognized are open relative file paths 2831 const info = this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName)); 2832 if (info) { 2833 return info; 2834 } 2835 2836 // This means triple slash references wont be resolved in dynamic and unsaved files 2837 // which is intentional since we dont know what it means to be relative to non disk files 2838 return undefined; 2839 } 2840 2841 private getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) { 2842 return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); 2843 } 2844 2845 getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) { 2846 return this.getOrCreateScriptInfoWorker(fileName, this.currentDirectory, openedByClient, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); 2847 } 2848 2849 private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) { 2850 Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content"); 2851 const path = normalizedPathToPath(fileName, currentDirectory, this.toCanonicalFileName); 2852 let info = this.getScriptInfoForPath(path); 2853 if (!info) { 2854 const isDynamic = isDynamicFileName(fileName); 2855 Debug.assert(isRootedDiskPath(fileName) || isDynamic || openedByClient, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nScript info with non-dynamic relative file name can only be open script info or in context of host currentDirectory`); 2856 Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nOpen script files with non rooted disk path opened with current directory context cannot have same canonical names`); 2857 Debug.assert(!isDynamic || this.currentDirectory === currentDirectory || this.useInferredProjectPerProjectRoot, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`); 2858 // If the file is not opened by client and the file doesnot exist on the disk, return 2859 if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) { 2860 return; 2861 } 2862 info = new ScriptInfo(this.host, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217 2863 this.filenameToScriptInfo.set(info.path, info); 2864 this.filenameToScriptInfoVersion.delete(info.path); 2865 if (!openedByClient) { 2866 this.watchClosedScriptInfo(info); 2867 } 2868 else if (!isRootedDiskPath(fileName) && (!isDynamic || this.currentDirectory !== currentDirectory)) { 2869 // File that is opened by user but isn't rooted disk path 2870 this.openFilesWithNonRootedDiskPath.set(this.toCanonicalFileName(fileName), info); 2871 } 2872 } 2873 if (openedByClient) { 2874 // Opening closed script info 2875 // either it was created just now, or was part of projects but was closed 2876 this.stopWatchingScriptInfo(info); 2877 info.open(fileContent!); 2878 if (hasMixedContent) { 2879 info.registerFileUpdate(); 2880 } 2881 } 2882 return info; 2883 } 2884 2885 /** 2886 * This gets the script info for the normalized path. If the path is not rooted disk path then the open script info with project root context is preferred 2887 */ 2888 getScriptInfoForNormalizedPath(fileName: NormalizedPath) { 2889 return !isRootedDiskPath(fileName) && this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName)) || 2890 this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName)); 2891 } 2892 2893 getScriptInfoForPath(fileName: Path) { 2894 return this.filenameToScriptInfo.get(fileName); 2895 } 2896 2897 /** @internal */ 2898 getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { 2899 // Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host 2900 const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host); 2901 if (!declarationInfo) { 2902 if (sourceFileName) { 2903 // Project contains source file and it generates the generated file name 2904 project.addGeneratedFileWatch(generatedFileName, sourceFileName); 2905 } 2906 return undefined; 2907 } 2908 2909 // Try to get from cache 2910 declarationInfo.getSnapshot(); // Ensure synchronized 2911 if (isString(declarationInfo.sourceMapFilePath)) { 2912 // Ensure mapper is synchronized 2913 const sourceMapFileInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath); 2914 if (sourceMapFileInfo) { 2915 sourceMapFileInfo.getSnapshot(); 2916 if (sourceMapFileInfo.documentPositionMapper !== undefined) { 2917 sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos); 2918 return sourceMapFileInfo.documentPositionMapper ? sourceMapFileInfo.documentPositionMapper : undefined; 2919 } 2920 } 2921 declarationInfo.sourceMapFilePath = undefined; 2922 } 2923 else if (declarationInfo.sourceMapFilePath) { 2924 declarationInfo.sourceMapFilePath.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, declarationInfo.sourceMapFilePath.sourceInfos); 2925 return undefined; 2926 } 2927 else if (declarationInfo.sourceMapFilePath !== undefined) { 2928 // Doesnt have sourceMap 2929 return undefined; 2930 } 2931 2932 // Create the mapper 2933 let sourceMapFileInfo: ScriptInfo | undefined; 2934 let mapFileNameFromDeclarationInfo: string | undefined; 2935 2936 let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => { 2937 const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host); 2938 if (!mapInfo) { 2939 mapFileNameFromDeclarationInfo = mapFileNameFromDts; 2940 return undefined; 2941 } 2942 sourceMapFileInfo = mapInfo; 2943 const snap = mapInfo.getSnapshot(); 2944 if (mapInfo.documentPositionMapper !== undefined) return mapInfo.documentPositionMapper; 2945 return getSnapshotText(snap); 2946 }; 2947 const projectName = project.projectName; 2948 const documentPositionMapper = getDocumentPositionMapper( 2949 { getCanonicalFileName: this.toCanonicalFileName, log: s => this.logger.info(s), getSourceFileLike: f => this.getSourceFileLike(f, projectName, declarationInfo) }, 2950 declarationInfo.fileName, 2951 declarationInfo.getLineInfo(), 2952 readMapFile 2953 ); 2954 readMapFile = undefined; // Remove ref to project 2955 if (sourceMapFileInfo) { 2956 declarationInfo.sourceMapFilePath = sourceMapFileInfo.path; 2957 sourceMapFileInfo.declarationInfoPath = declarationInfo.path; 2958 sourceMapFileInfo.documentPositionMapper = documentPositionMapper || false; 2959 sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos); 2960 } 2961 else if (mapFileNameFromDeclarationInfo) { 2962 declarationInfo.sourceMapFilePath = { 2963 watcher: this.addMissingSourceMapFile( 2964 project.currentDirectory === this.currentDirectory ? 2965 mapFileNameFromDeclarationInfo : 2966 getNormalizedAbsolutePath(mapFileNameFromDeclarationInfo, project.currentDirectory), 2967 declarationInfo.path 2968 ), 2969 sourceInfos: this.addSourceInfoToSourceMap(sourceFileName, project) 2970 }; 2971 } 2972 else { 2973 declarationInfo.sourceMapFilePath = false; 2974 } 2975 return documentPositionMapper; 2976 } 2977 2978 private addSourceInfoToSourceMap(sourceFileName: string | undefined, project: Project, sourceInfos?: Set<Path>) { 2979 if (sourceFileName) { 2980 // Attach as source 2981 const sourceInfo = this.getOrCreateScriptInfoNotOpenedByClient(sourceFileName, project.currentDirectory, project.directoryStructureHost)!; 2982 (sourceInfos || (sourceInfos = new Set())).add(sourceInfo.path); 2983 } 2984 return sourceInfos; 2985 } 2986 2987 private addMissingSourceMapFile(mapFileName: string, declarationInfoPath: Path) { 2988 const fileWatcher = this.watchFactory.watchFile( 2989 mapFileName, 2990 () => { 2991 const declarationInfo = this.getScriptInfoForPath(declarationInfoPath); 2992 if (declarationInfo && declarationInfo.sourceMapFilePath && !isString(declarationInfo.sourceMapFilePath)) { 2993 // Update declaration and source projects 2994 this.delayUpdateProjectGraphs(declarationInfo.containingProjects, /*clearSourceMapperCache*/ true); 2995 this.delayUpdateSourceInfoProjects(declarationInfo.sourceMapFilePath.sourceInfos); 2996 declarationInfo.closeSourceMapFileWatcher(); 2997 } 2998 }, 2999 PollingInterval.High, 3000 this.hostConfiguration.watchOptions, 3001 WatchType.MissingSourceMapFile, 3002 ); 3003 return fileWatcher; 3004 } 3005 3006 /** @internal */ 3007 getSourceFileLike(fileName: string, projectNameOrProject: string | Project, declarationInfo?: ScriptInfo): SourceFileLike | undefined { 3008 const project = (projectNameOrProject as Project).projectName ? projectNameOrProject as Project : this.findProject(projectNameOrProject as string); 3009 if (project) { 3010 const path = project.toPath(fileName); 3011 const sourceFile = project.getSourceFile(path); 3012 if (sourceFile && sourceFile.resolvedPath === path) return sourceFile; 3013 } 3014 3015 // Need to look for other files. 3016 const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host); 3017 if (!info) return undefined; 3018 3019 // Attach as source 3020 if (declarationInfo && isString(declarationInfo.sourceMapFilePath) && info !== declarationInfo) { 3021 const sourceMapInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath); 3022 if (sourceMapInfo) { 3023 (sourceMapInfo.sourceInfos || (sourceMapInfo.sourceInfos = new Set())).add(info.path); 3024 } 3025 } 3026 3027 // Key doesnt matter since its only for text and lines 3028 if (info.cacheSourceFile) return info.cacheSourceFile.sourceFile; 3029 3030 // Create sourceFileLike 3031 if (!info.sourceFileLike) { 3032 info.sourceFileLike = { 3033 get text() { 3034 Debug.fail("shouldnt need text"); 3035 return ""; 3036 }, 3037 getLineAndCharacterOfPosition: pos => { 3038 const lineOffset = info.positionToLineOffset(pos); 3039 return { line: lineOffset.line - 1, character: lineOffset.offset - 1 }; 3040 }, 3041 getPositionOfLineAndCharacter: (line, character, allowEdits) => info.lineOffsetToPosition(line + 1, character + 1, allowEdits) 3042 }; 3043 } 3044 return info.sourceFileLike; 3045 } 3046 3047 /** @internal */ 3048 setPerformanceEventHandler(performanceEventHandler: PerformanceEventHandler) { 3049 this.performanceEventHandler = performanceEventHandler; 3050 } 3051 3052 setHostConfiguration(args: protocol.ConfigureRequestArguments) { 3053 if (args.file) { 3054 const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); 3055 if (info) { 3056 info.setOptions(convertFormatOptions(args.formatOptions!), args.preferences); 3057 this.logger.info(`Host configuration update for file ${args.file}`); 3058 } 3059 } 3060 else { 3061 if (args.hostInfo !== undefined) { 3062 this.hostConfiguration.hostInfo = args.hostInfo; 3063 this.logger.info(`Host information ${args.hostInfo}`); 3064 } 3065 if (args.formatOptions) { 3066 this.hostConfiguration.formatCodeOptions = { ...this.hostConfiguration.formatCodeOptions, ...convertFormatOptions(args.formatOptions) }; 3067 this.logger.info("Format host information updated"); 3068 } 3069 if (args.preferences) { 3070 const { 3071 lazyConfiguredProjectsFromExternalProject, 3072 includePackageJsonAutoImports, 3073 } = this.hostConfiguration.preferences; 3074 3075 this.hostConfiguration.preferences = { ...this.hostConfiguration.preferences, ...args.preferences }; 3076 if (lazyConfiguredProjectsFromExternalProject && !this.hostConfiguration.preferences.lazyConfiguredProjectsFromExternalProject) { 3077 // Load configured projects for external projects that are pending reload 3078 this.configuredProjects.forEach(project => { 3079 if (project.hasExternalProjectRef() && 3080 project.pendingReload === ConfigFileProgramReloadLevel.Full && 3081 !this.pendingProjectUpdates.has(project.getProjectName())) { 3082 project.updateGraph(); 3083 } 3084 }); 3085 } 3086 if (includePackageJsonAutoImports !== args.preferences.includePackageJsonAutoImports) { 3087 this.invalidateProjectPackageJson(/*packageJsonPath*/ undefined); 3088 } 3089 } 3090 if (args.extraFileExtensions) { 3091 this.hostConfiguration.extraFileExtensions = args.extraFileExtensions; 3092 // We need to update the project structures again as it is possible that existing 3093 // project structure could have more or less files depending on extensions permitted 3094 this.reloadProjects(); 3095 this.logger.info("Host file extension mappings updated"); 3096 } 3097 3098 if (args.watchOptions) { 3099 this.hostConfiguration.watchOptions = convertWatchOptions(args.watchOptions)?.watchOptions; 3100 this.logger.info(`Host watch options changed to ${JSON.stringify(this.hostConfiguration.watchOptions)}, it will be take effect for next watches.`); 3101 } 3102 } 3103 } 3104 3105 /** @internal */ 3106 getWatchOptions(project: Project) { 3107 return this.getWatchOptionsFromProjectWatchOptions(project.getWatchOptions()); 3108 } 3109 3110 /** @internal */ 3111 private getWatchOptionsFromProjectWatchOptions(projectOptions: WatchOptions | undefined) { 3112 return projectOptions && this.hostConfiguration.watchOptions ? 3113 { ...this.hostConfiguration.watchOptions, ...projectOptions } : 3114 projectOptions || this.hostConfiguration.watchOptions; 3115 } 3116 3117 closeLog() { 3118 this.logger.close(); 3119 } 3120 3121 /** 3122 * This function rebuilds the project for every file opened by the client 3123 * This does not reload contents of open files from disk. But we could do that if needed 3124 */ 3125 reloadProjects() { 3126 this.logger.info("reload projects."); 3127 // If we want this to also reload open files from disk, we could do that, 3128 // but then we need to make sure we arent calling this function 3129 // (and would separate out below reloading of projects to be called when immediate reload is needed) 3130 // as there is no need to load contents of the files from the disk 3131 3132 // Reload script infos 3133 this.filenameToScriptInfo.forEach(info => { 3134 if (this.openFiles.has(info.path)) return; // Skip open files 3135 if (!info.fileWatcher) return; // not watched file 3136 // Handle as if file is changed or deleted 3137 this.onSourceFileChanged(info, this.host.fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted); 3138 }); 3139 // Cancel all project updates since we will be updating them now 3140 this.pendingProjectUpdates.forEach((_project, projectName) => { 3141 this.throttledOperations.cancel(projectName); 3142 this.pendingProjectUpdates.delete(projectName); 3143 }); 3144 this.throttledOperations.cancel(ensureProjectForOpenFileSchedule); 3145 this.pendingEnsureProjectForOpenFiles = false; 3146 3147 // Ensure everything is reloaded for cached configs 3148 this.configFileExistenceInfoCache.forEach(info => { 3149 if (info.config) info.config.reloadLevel = ConfigFileProgramReloadLevel.Full; 3150 }); 3151 3152 // Reload Projects 3153 this.reloadConfiguredProjectForFiles(this.openFiles as ESMap<Path, NormalizedPath | undefined>, /*clearSemanticCache*/ true, /*delayReload*/ false, returnTrue, "User requested reload projects"); 3154 this.externalProjects.forEach(project => { 3155 this.clearSemanticCache(project); 3156 project.updateGraph(); 3157 }); 3158 this.inferredProjects.forEach(project => this.clearSemanticCache(project)); 3159 this.ensureProjectForOpenFiles(); 3160 } 3161 3162 /** 3163 * This function goes through all the openFiles and tries to file the config file for them. 3164 * If the config file is found and it refers to existing project, it reloads it either immediately 3165 * or schedules it for reload depending on delayReload option 3166 * If there is no existing project it just opens the configured project for the config file 3167 * reloadForInfo provides a way to filter out files to reload configured project for 3168 */ 3169 private reloadConfiguredProjectForFiles<T>(openFiles: ESMap<Path, T> | undefined, clearSemanticCache: boolean, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean, reason: string) { 3170 const updatedProjects = new Map<string, true>(); 3171 const reloadChildProject = (child: ConfiguredProject) => { 3172 if (!updatedProjects.has(child.canonicalConfigFilePath)) { 3173 updatedProjects.set(child.canonicalConfigFilePath, true); 3174 this.reloadConfiguredProject(child, reason, /*isInitialLoad*/ false, clearSemanticCache); 3175 } 3176 }; 3177 // try to reload config file for all open files 3178 openFiles?.forEach((openFileValue, path) => { 3179 // Invalidate default config file name for open file 3180 this.configFileForOpenFiles.delete(path); 3181 // Filter out the files that need to be ignored 3182 if (!shouldReloadProjectFor(openFileValue)) { 3183 return; 3184 } 3185 3186 const info = this.getScriptInfoForPath(path)!; // TODO: GH#18217 3187 Debug.assert(info.isScriptOpen()); 3188 // This tries to search for a tsconfig.json for the given file. If we found it, 3189 // we first detect if there is already a configured project created for it: if so, 3190 // we re- read the tsconfig file content and update the project only if we havent already done so 3191 // otherwise we create a new one. 3192 const configFileName = this.getConfigFileNameForFile(info); 3193 if (configFileName) { 3194 const project = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName); 3195 if (!updatedProjects.has(project.canonicalConfigFilePath)) { 3196 updatedProjects.set(project.canonicalConfigFilePath, true); 3197 if (delayReload) { 3198 project.pendingReload = ConfigFileProgramReloadLevel.Full; 3199 project.pendingReloadReason = reason; 3200 if (clearSemanticCache) this.clearSemanticCache(project); 3201 this.delayUpdateProjectGraph(project); 3202 } 3203 else { 3204 // reload from the disk 3205 this.reloadConfiguredProject(project, reason, /*isInitialLoad*/ false, clearSemanticCache); 3206 // If this project does not contain this file directly, reload the project till the reloaded project contains the script info directly 3207 if (!projectContainsInfoDirectly(project, info)) { 3208 const referencedProject = forEachResolvedProjectReferenceProject( 3209 project, 3210 info.path, 3211 child => { 3212 reloadChildProject(child); 3213 return projectContainsInfoDirectly(child, info); 3214 }, 3215 ProjectReferenceProjectLoadKind.FindCreate 3216 ); 3217 if (referencedProject) { 3218 // Reload the project's tree that is already present 3219 forEachResolvedProjectReferenceProject( 3220 project, 3221 /*fileName*/ undefined, 3222 reloadChildProject, 3223 ProjectReferenceProjectLoadKind.Find 3224 ); 3225 } 3226 } 3227 } 3228 } 3229 } 3230 }); 3231 } 3232 3233 /** 3234 * Remove the root of inferred project if script info is part of another project 3235 */ 3236 private removeRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { 3237 // If the script info is root of inferred project, it could only be first containing project 3238 // since info is added as root to the inferred project only when there are no other projects containing it 3239 // So when it is root of the inferred project and after project structure updates its now part 3240 // of multiple project it needs to be removed from that inferred project because: 3241 // - references in inferred project supersede the root part 3242 // - root / reference in non - inferred project beats root in inferred project 3243 3244 // eg. say this is structure /a/b/a.ts /a/b/c.ts where c.ts references a.ts 3245 // When a.ts is opened, since there is no configured project/external project a.ts can be part of 3246 // a.ts is added as root to inferred project. 3247 // Now at time of opening c.ts, c.ts is also not aprt of any existing project, 3248 // so it will be added to inferred project as a root. (for sake of this example assume single inferred project is false) 3249 // So at this poing a.ts is part of first inferred project and second inferred project (of which c.ts is root) 3250 // And hence it needs to be removed from the first inferred project. 3251 Debug.assert(info.containingProjects.length > 0); 3252 const firstProject = info.containingProjects[0]; 3253 3254 if (!firstProject.isOrphan() && 3255 isInferredProject(firstProject) && 3256 firstProject.isRoot(info) && 3257 forEach(info.containingProjects, p => p !== firstProject && !p.isOrphan())) { 3258 firstProject.removeFile(info, /*fileExists*/ true, /*detachFromProject*/ true); 3259 } 3260 } 3261 3262 /** 3263 * This function is to update the project structure for every inferred project. 3264 * It is called on the premise that all the configured projects are 3265 * up to date. 3266 * This will go through open files and assign them to inferred project if open file is not part of any other project 3267 * After that all the inferred project graphs are updated 3268 */ 3269 private ensureProjectForOpenFiles() { 3270 this.logger.info("Before ensureProjectForOpenFiles:"); 3271 this.printProjects(); 3272 3273 this.openFiles.forEach((projectRootPath, path) => { 3274 const info = this.getScriptInfoForPath(path as Path)!; 3275 // collect all orphaned script infos from open files 3276 if (info.isOrphan()) { 3277 this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); 3278 } 3279 else { 3280 // Or remove the root of inferred project if is referenced in more than one projects 3281 this.removeRootOfInferredProjectIfNowPartOfOtherProject(info); 3282 } 3283 }); 3284 this.pendingEnsureProjectForOpenFiles = false; 3285 this.inferredProjects.forEach(updateProjectIfDirty); 3286 3287 this.logger.info("After ensureProjectForOpenFiles:"); 3288 this.printProjects(); 3289 } 3290 3291 /** 3292 * Open file whose contents is managed by the client 3293 * @param filename is absolute pathname 3294 * @param fileContent is a known version of the file content that is more up to date than the one on disk 3295 */ 3296 openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: string): OpenConfiguredProjectResult { 3297 return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath ? toNormalizedPath(projectRootPath) : undefined); 3298 } 3299 3300 /** @internal */ 3301 getOriginalLocationEnsuringConfiguredProject(project: Project, location: DocumentPosition): DocumentPosition | undefined { 3302 const isSourceOfProjectReferenceRedirect = project.isSourceOfProjectReferenceRedirect(location.fileName); 3303 const originalLocation = isSourceOfProjectReferenceRedirect ? 3304 location : 3305 project.getSourceMapper().tryGetSourcePosition(location); 3306 if (!originalLocation) return undefined; 3307 3308 const { fileName } = originalLocation; 3309 const scriptInfo = this.getScriptInfo(fileName); 3310 if (!scriptInfo && !this.host.fileExists(fileName)) return undefined; 3311 3312 const originalFileInfo: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) }; 3313 const configFileName = this.getConfigFileNameForFile(originalFileInfo); 3314 if (!configFileName) return undefined; 3315 3316 let configuredProject: ConfiguredProject | undefined = this.findConfiguredProjectByProjectName(configFileName); 3317 if (!configuredProject) { 3318 if (project.getCompilerOptions().disableReferencedProjectLoad) { 3319 // If location was a project reference redirect, then `location` and `originalLocation` are the same. 3320 if (isSourceOfProjectReferenceRedirect) { 3321 return location; 3322 } 3323 3324 // Otherwise, if we found `originalLocation` via a source map instead, then we check whether it's in 3325 // an open project. If it is, we should search the containing project(s), even though the "default" 3326 // configured project isn't open. However, if it's not in an open project, we need to stick with 3327 // `location` (i.e. the .d.ts file) because otherwise we'll miss the references in that file. 3328 return scriptInfo?.containingProjects.length 3329 ? originalLocation 3330 : location; 3331 } 3332 3333 configuredProject = this.createAndLoadConfiguredProject(configFileName, `Creating project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}`); 3334 } 3335 updateProjectIfDirty(configuredProject); 3336 3337 const projectContainsOriginalInfo = (project: ConfiguredProject) => { 3338 const info = this.getScriptInfo(fileName); 3339 return info && projectContainsInfoDirectly(project, info); 3340 }; 3341 3342 if (configuredProject.isSolution() || !projectContainsOriginalInfo(configuredProject)) { 3343 // Find the project that is referenced from this solution that contains the script info directly 3344 configuredProject = forEachResolvedProjectReferenceProject( 3345 configuredProject, 3346 fileName, 3347 child => { 3348 updateProjectIfDirty(child); 3349 return projectContainsOriginalInfo(child) ? child : undefined; 3350 }, 3351 ProjectReferenceProjectLoadKind.FindCreateLoad, 3352 `Creating project referenced in solution ${configuredProject.projectName} to find possible configured project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}` 3353 ); 3354 if (!configuredProject) return undefined; 3355 if (configuredProject === project) return originalLocation; 3356 } 3357 3358 // Keep this configured project as referenced from project 3359 addOriginalConfiguredProject(configuredProject); 3360 3361 const originalScriptInfo = this.getScriptInfo(fileName); 3362 if (!originalScriptInfo || !originalScriptInfo.containingProjects.length) return undefined; 3363 3364 // Add configured projects as referenced 3365 originalScriptInfo.containingProjects.forEach(project => { 3366 if (isConfiguredProject(project)) { 3367 addOriginalConfiguredProject(project); 3368 } 3369 }); 3370 return originalLocation; 3371 3372 function addOriginalConfiguredProject(originalProject: ConfiguredProject) { 3373 if (!project.originalConfiguredProjects) { 3374 project.originalConfiguredProjects = new Set(); 3375 } 3376 project.originalConfiguredProjects.add(originalProject.canonicalConfigFilePath); 3377 } 3378 } 3379 3380 /** @internal */ 3381 fileExists(fileName: NormalizedPath): boolean { 3382 return !!this.getScriptInfoForNormalizedPath(fileName) || this.host.fileExists(fileName); 3383 } 3384 3385 private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined { 3386 return find(this.externalProjects, proj => { 3387 // Ensure project structure is up-to-date to check if info is present in external project 3388 updateProjectIfDirty(proj); 3389 return proj.containsScriptInfo(info); 3390 }); 3391 } 3392 3393 private getOrCreateOpenScriptInfo(fileName: NormalizedPath, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, projectRootPath: NormalizedPath | undefined) { 3394 const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent)!; // TODO: GH#18217 3395 this.openFiles.set(info.path, projectRootPath); 3396 return info; 3397 } 3398 3399 private assignProjectToOpenedScriptInfo(info: ScriptInfo): AssignProjectResult { 3400 let configFileName: NormalizedPath | undefined; 3401 let configFileErrors: readonly Diagnostic[] | undefined; 3402 let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info); 3403 let retainProjects: ConfiguredProject[] | ConfiguredProject | undefined; 3404 let projectForConfigFileDiag: ConfiguredProject | undefined; 3405 let defaultConfigProjectIsCreated = false; 3406 if (!project && this.serverMode === LanguageServiceMode.Semantic) { // Checking semantic mode is an optimization 3407 configFileName = this.getConfigFileNameForFile(info); 3408 if (configFileName) { 3409 project = this.findConfiguredProjectByProjectName(configFileName); 3410 if (!project) { 3411 project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${info.fileName} to open`); 3412 defaultConfigProjectIsCreated = true; 3413 } 3414 else { 3415 // Ensure project is ready to check if it contains opened script info 3416 updateProjectIfDirty(project); 3417 } 3418 3419 projectForConfigFileDiag = project.containsScriptInfo(info) ? project : undefined; 3420 retainProjects = project; 3421 3422 // If this configured project doesnt contain script info but 3423 // it is solution with project references, try those project references 3424 if (!projectContainsInfoDirectly(project, info)) { 3425 forEachResolvedProjectReferenceProject( 3426 project, 3427 info.path, 3428 child => { 3429 updateProjectIfDirty(child); 3430 // Retain these projects 3431 if (!isArray(retainProjects)) { 3432 retainProjects = [project as ConfiguredProject, child]; 3433 } 3434 else { 3435 retainProjects.push(child); 3436 } 3437 3438 // If script info belongs to this child project, use this as default config project 3439 if (projectContainsInfoDirectly(child, info)) { 3440 projectForConfigFileDiag = child; 3441 return child; 3442 } 3443 3444 // If this project uses the script info (even through project reference), if default project is not found, use this for configFileDiag 3445 if (!projectForConfigFileDiag && child.containsScriptInfo(info)) { 3446 projectForConfigFileDiag = child; 3447 } 3448 }, 3449 ProjectReferenceProjectLoadKind.FindCreateLoad, 3450 `Creating project referenced in solution ${project.projectName} to find possible configured project for ${info.fileName} to open` 3451 ); 3452 } 3453 3454 // Send the event only if the project got created as part of this open request and info is part of the project 3455 if (projectForConfigFileDiag) { 3456 configFileName = projectForConfigFileDiag.getConfigFilePath(); 3457 if (projectForConfigFileDiag !== project || defaultConfigProjectIsCreated) { 3458 configFileErrors = projectForConfigFileDiag.getAllProjectErrors(); 3459 this.sendConfigFileDiagEvent(projectForConfigFileDiag, info.fileName); 3460 } 3461 } 3462 else { 3463 // Since the file isnt part of configured project, do not send config file info 3464 configFileName = undefined; 3465 } 3466 3467 // Create ancestor configured project 3468 this.createAncestorProjects(info, project); 3469 } 3470 } 3471 3472 // Project we have at this point is going to be updated since its either found through 3473 // - external project search, which updates the project before checking if info is present in it 3474 // - configured project - either created or updated to ensure we know correct status of info 3475 3476 // At this point we need to ensure that containing projects of the info are uptodate 3477 // This will ensure that later question of info.isOrphan() will return correct answer 3478 // and we correctly create inferred project for the info 3479 info.containingProjects.forEach(updateProjectIfDirty); 3480 3481 // At this point if file is part of any any configured or external project, then it would be present in the containing projects 3482 // So if it still doesnt have any containing projects, it needs to be part of inferred project 3483 if (info.isOrphan()) { 3484 // Even though this info did not belong to any of the configured projects, send the config file diag 3485 if (isArray(retainProjects)) { 3486 retainProjects.forEach(project => this.sendConfigFileDiagEvent(project, info.fileName)); 3487 } 3488 else if (retainProjects) { 3489 this.sendConfigFileDiagEvent(retainProjects, info.fileName); 3490 } 3491 Debug.assert(this.openFiles.has(info.path)); 3492 this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path)); 3493 } 3494 Debug.assert(!info.isOrphan()); 3495 return { configFileName, configFileErrors, retainProjects }; 3496 } 3497 3498 private createAncestorProjects(info: ScriptInfo, project: ConfiguredProject) { 3499 // Skip if info is not part of default configured project 3500 if (!info.isAttached(project)) return; 3501 3502 // Create configured project till project root 3503 while (true) { 3504 // Skip if project is not composite 3505 if (!project.isInitialLoadPending() && 3506 ( 3507 !project.getCompilerOptions().composite || 3508 project.getCompilerOptions().disableSolutionSearching 3509 ) 3510 ) return; 3511 3512 // Get config file name 3513 const configFileName = this.getConfigFileNameForFile({ 3514 fileName: project.getConfigFilePath(), 3515 path: info.path, 3516 configFileInfo: true 3517 }); 3518 if (!configFileName) return; 3519 3520 // find or delay load the project 3521 const ancestor = this.findConfiguredProjectByProjectName(configFileName) || 3522 this.createConfiguredProjectWithDelayLoad(configFileName, `Creating project possibly referencing default composite project ${project.getProjectName()} of open file ${info.fileName}`); 3523 if (ancestor.isInitialLoadPending()) { 3524 // Set a potential project reference 3525 ancestor.setPotentialProjectReference(project.canonicalConfigFilePath); 3526 } 3527 project = ancestor; 3528 } 3529 } 3530 3531 /** @internal */ 3532 loadAncestorProjectTree(forProjects?: ReadonlyCollection<string>) { 3533 forProjects = forProjects || mapDefinedEntries( 3534 this.configuredProjects, 3535 (key, project) => !project.isInitialLoadPending() ? [key, true] : undefined 3536 ); 3537 3538 const seenProjects = new Set<NormalizedPath>(); 3539 // Work on array copy as we could add more projects as part of callback 3540 for (const project of arrayFrom(this.configuredProjects.values())) { 3541 // If this project has potential project reference for any of the project we are loading ancestor tree for 3542 // load this project first 3543 if (forEachPotentialProjectReference(project, potentialRefPath => forProjects!.has(potentialRefPath))) { 3544 updateProjectIfDirty(project); 3545 } 3546 this.ensureProjectChildren(project, forProjects, seenProjects); 3547 } 3548 } 3549 3550 private ensureProjectChildren(project: ConfiguredProject, forProjects: ReadonlyCollection<string>, seenProjects: Set<NormalizedPath>) { 3551 if (!tryAddToSet(seenProjects, project.canonicalConfigFilePath)) return; 3552 3553 // If this project disables child load ignore it 3554 if (project.getCompilerOptions().disableReferencedProjectLoad) return; 3555 3556 const children = project.getCurrentProgram()?.getResolvedProjectReferences(); 3557 if (!children) return; 3558 3559 for (const child of children) { 3560 if (!child) continue; 3561 const referencedProject = forEachResolvedProjectReference(child.references, ref => forProjects.has(ref.sourceFile.path) ? ref : undefined); 3562 if (!referencedProject) continue; 3563 3564 // Load this project, 3565 const configFileName = toNormalizedPath(child.sourceFile.fileName); 3566 const childProject = project.projectService.findConfiguredProjectByProjectName(configFileName) || 3567 project.projectService.createAndLoadConfiguredProject(configFileName, `Creating project referenced by : ${project.projectName} as it references project ${referencedProject.sourceFile.fileName}`); 3568 updateProjectIfDirty(childProject); 3569 3570 // Ensure children for this project 3571 this.ensureProjectChildren(childProject, forProjects, seenProjects); 3572 } 3573 } 3574 3575 private cleanupAfterOpeningFile(toRetainConfigProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) { 3576 // This was postponed from closeOpenFile to after opening next file, 3577 // so that we can reuse the project if we need to right away 3578 this.removeOrphanConfiguredProjects(toRetainConfigProjects); 3579 3580 // Remove orphan inferred projects now that we have reused projects 3581 // We need to create a duplicate because we cant guarantee order after removal 3582 for (const inferredProject of this.inferredProjects.slice()) { 3583 if (inferredProject.isOrphan()) { 3584 this.removeProject(inferredProject); 3585 } 3586 } 3587 3588 // Delete the orphan files here because there might be orphan script infos (which are not part of project) 3589 // when some file/s were closed which resulted in project removal. 3590 // It was then postponed to cleanup these script infos so that they can be reused if 3591 // the file from that old project is reopened because of opening file from here. 3592 this.removeOrphanScriptInfos(); 3593 } 3594 3595 openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult { 3596 const info = this.getOrCreateOpenScriptInfo(fileName, fileContent, scriptKind, hasMixedContent, projectRootPath); 3597 const { retainProjects, ...result } = this.assignProjectToOpenedScriptInfo(info); 3598 this.cleanupAfterOpeningFile(retainProjects); 3599 this.telemetryOnOpenFile(info); 3600 this.printProjects(); 3601 return result; 3602 } 3603 3604 private removeOrphanConfiguredProjects(toRetainConfiguredProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) { 3605 const toRemoveConfiguredProjects = new Map(this.configuredProjects); 3606 const markOriginalProjectsAsUsed = (project: Project) => { 3607 if (!project.isOrphan() && project.originalConfiguredProjects) { 3608 project.originalConfiguredProjects.forEach( 3609 (_value, configuredProjectPath) => { 3610 const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath); 3611 return project && retainConfiguredProject(project); 3612 } 3613 ); 3614 } 3615 }; 3616 if (toRetainConfiguredProjects) { 3617 if (isArray(toRetainConfiguredProjects)) { 3618 toRetainConfiguredProjects.forEach(retainConfiguredProject); 3619 } 3620 else { 3621 retainConfiguredProject(toRetainConfiguredProjects); 3622 } 3623 } 3624 3625 // Do not remove configured projects that are used as original projects of other 3626 this.inferredProjects.forEach(markOriginalProjectsAsUsed); 3627 this.externalProjects.forEach(markOriginalProjectsAsUsed); 3628 this.configuredProjects.forEach(project => { 3629 // If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references 3630 if (project.hasOpenRef()) { 3631 retainConfiguredProject(project); 3632 } 3633 else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) { 3634 // If the configured project for project reference has more than zero references, keep it alive 3635 forEachReferencedProject( 3636 project, 3637 ref => isRetained(ref) && retainConfiguredProject(project) 3638 ); 3639 } 3640 }); 3641 3642 // Remove all the non marked projects 3643 toRemoveConfiguredProjects.forEach(project => this.removeProject(project)); 3644 3645 function isRetained(project: ConfiguredProject) { 3646 return project.hasOpenRef() || !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath); 3647 } 3648 3649 function retainConfiguredProject(project: ConfiguredProject) { 3650 if (toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath)) { 3651 // Keep original projects used 3652 markOriginalProjectsAsUsed(project); 3653 // Keep all the references alive 3654 forEachReferencedProject(project, retainConfiguredProject); 3655 } 3656 } 3657 } 3658 3659 private removeOrphanScriptInfos() { 3660 const toRemoveScriptInfos = new Map(this.filenameToScriptInfo); 3661 this.filenameToScriptInfo.forEach(info => { 3662 // If script info is open or orphan, retain it and its dependencies 3663 if (!info.isScriptOpen() && info.isOrphan() && !info.isContainedByBackgroundProject()) { 3664 // Otherwise if there is any source info that is alive, this alive too 3665 if (!info.sourceMapFilePath) return; 3666 let sourceInfos: Set<Path> | undefined; 3667 if (isString(info.sourceMapFilePath)) { 3668 const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath); 3669 sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos; 3670 } 3671 else { 3672 sourceInfos = info.sourceMapFilePath.sourceInfos; 3673 } 3674 if (!sourceInfos) return; 3675 if (!forEachKey(sourceInfos, path => { 3676 const info = this.getScriptInfoForPath(path); 3677 return !!info && (info.isScriptOpen() || !info.isOrphan()); 3678 })) { 3679 return; 3680 } 3681 } 3682 3683 // Retain this script info 3684 toRemoveScriptInfos.delete(info.path); 3685 if (info.sourceMapFilePath) { 3686 let sourceInfos: Set<Path> | undefined; 3687 if (isString(info.sourceMapFilePath)) { 3688 // And map file info and source infos 3689 toRemoveScriptInfos.delete(info.sourceMapFilePath); 3690 const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath); 3691 sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos; 3692 } 3693 else { 3694 sourceInfos = info.sourceMapFilePath.sourceInfos; 3695 } 3696 if (sourceInfos) { 3697 sourceInfos.forEach((_value, path) => toRemoveScriptInfos.delete(path)); 3698 } 3699 } 3700 }); 3701 3702 toRemoveScriptInfos.forEach(info => { 3703 // if there are not projects that include this script info - delete it 3704 this.stopWatchingScriptInfo(info); 3705 this.deleteScriptInfo(info); 3706 info.closeSourceMapFileWatcher(); 3707 }); 3708 } 3709 3710 private telemetryOnOpenFile(scriptInfo: ScriptInfo): void { 3711 if (this.serverMode !== LanguageServiceMode.Semantic || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) { 3712 return; 3713 } 3714 3715 const project = this.ensureDefaultProjectForFile(scriptInfo); 3716 if (!project.languageServiceEnabled) { 3717 return; 3718 } 3719 3720 const sourceFile = project.getSourceFile(scriptInfo.path); 3721 const checkJs = !!sourceFile && !!sourceFile.checkJsDirective; 3722 this.eventHandler({ eventName: OpenFileInfoTelemetryEvent, data: { info: { checkJs } } }); 3723 } 3724 3725 /** 3726 * Close file whose contents is managed by the client 3727 * @param filename is absolute pathname 3728 */ 3729 closeClientFile(uncheckedFileName: string): void; 3730 /** @internal */ 3731 closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject: true): boolean; 3732 closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject?: true) { 3733 const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); 3734 const result = info ? this.closeOpenFile(info, skipAssignOrphanScriptInfosToInferredProject) : false; 3735 if (!skipAssignOrphanScriptInfosToInferredProject) { 3736 this.printProjects(); 3737 } 3738 return result; 3739 } 3740 3741 private collectChanges( 3742 lastKnownProjectVersions: protocol.ProjectVersionInfo[], 3743 currentProjects: Project[], 3744 includeProjectReferenceRedirectInfo: boolean | undefined, 3745 result: ProjectFilesWithTSDiagnostics[] 3746 ): void { 3747 for (const proj of currentProjects) { 3748 const knownProject = find(lastKnownProjectVersions, p => p.projectName === proj.getProjectName()); 3749 result.push(proj.getChangesSinceVersion(knownProject && knownProject.version, includeProjectReferenceRedirectInfo)); 3750 } 3751 } 3752 3753 /** @internal */ 3754 synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[], includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics[] { 3755 const files: ProjectFilesWithTSDiagnostics[] = []; 3756 this.collectChanges(knownProjects, this.externalProjects, includeProjectReferenceRedirectInfo, files); 3757 this.collectChanges(knownProjects, arrayFrom(this.configuredProjects.values()), includeProjectReferenceRedirectInfo, files); 3758 this.collectChanges(knownProjects, this.inferredProjects, includeProjectReferenceRedirectInfo, files); 3759 return files; 3760 } 3761 3762 /** @internal */ 3763 applyChangesInOpenFiles(openFiles: Iterator<OpenFileArguments> | undefined, changedFiles?: Iterator<ChangeFileArguments>, closedFiles?: string[]): void { 3764 let openScriptInfos: ScriptInfo[] | undefined; 3765 let assignOrphanScriptInfosToInferredProject = false; 3766 if (openFiles) { 3767 while (true) { 3768 const iterResult = openFiles.next(); 3769 if (iterResult.done) break; 3770 const file = iterResult.value; 3771 // Create script infos so we have the new content for all the open files before we do any updates to projects 3772 const info = this.getOrCreateOpenScriptInfo( 3773 toNormalizedPath(file.fileName), 3774 file.content, 3775 tryConvertScriptKindName(file.scriptKind!), 3776 file.hasMixedContent, 3777 file.projectRootPath ? toNormalizedPath(file.projectRootPath) : undefined 3778 ); 3779 (openScriptInfos || (openScriptInfos = [])).push(info); 3780 } 3781 } 3782 3783 if (changedFiles) { 3784 while (true) { 3785 const iterResult = changedFiles.next(); 3786 if (iterResult.done) break; 3787 const file = iterResult.value; 3788 const scriptInfo = this.getScriptInfo(file.fileName)!; 3789 Debug.assert(!!scriptInfo); 3790 // Make edits to script infos and marks containing project as dirty 3791 this.applyChangesToFile(scriptInfo, file.changes); 3792 } 3793 } 3794 3795 if (closedFiles) { 3796 for (const file of closedFiles) { 3797 // Close files, but dont assign projects to orphan open script infos, that part comes later 3798 assignOrphanScriptInfosToInferredProject = this.closeClientFile(file, /*skipAssignOrphanScriptInfosToInferredProject*/ true) || assignOrphanScriptInfosToInferredProject; 3799 } 3800 } 3801 3802 // All the script infos now exist, so ok to go update projects for open files 3803 let retainProjects: readonly ConfiguredProject[] | undefined; 3804 if (openScriptInfos) { 3805 retainProjects = flatMap(openScriptInfos, info => this.assignProjectToOpenedScriptInfo(info).retainProjects); 3806 } 3807 3808 // While closing files there could be open files that needed assigning new inferred projects, do it now 3809 if (assignOrphanScriptInfosToInferredProject) { 3810 this.assignOrphanScriptInfosToInferredProject(); 3811 } 3812 3813 if (openScriptInfos) { 3814 // Cleanup projects 3815 this.cleanupAfterOpeningFile(retainProjects); 3816 // Telemetry 3817 openScriptInfos.forEach(info => this.telemetryOnOpenFile(info)); 3818 this.printProjects(); 3819 } 3820 else if (length(closedFiles)) { 3821 this.printProjects(); 3822 } 3823 } 3824 3825 /** @internal */ 3826 applyChangesToFile(scriptInfo: ScriptInfo, changes: Iterator<TextChange>) { 3827 while (true) { 3828 const iterResult = changes.next(); 3829 if (iterResult.done) break; 3830 const change = iterResult.value; 3831 scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); 3832 } 3833 } 3834 3835 private closeConfiguredProjectReferencedFromExternalProject(configFile: NormalizedPath) { 3836 const configuredProject = this.findConfiguredProjectByProjectName(configFile); 3837 if (configuredProject) { 3838 configuredProject.deleteExternalProjectReference(); 3839 if (!configuredProject.hasOpenRef()) { 3840 this.removeProject(configuredProject); 3841 return; 3842 } 3843 } 3844 } 3845 3846 closeExternalProject(uncheckedFileName: string): void { 3847 const fileName = toNormalizedPath(uncheckedFileName); 3848 const configFiles = this.externalProjectToConfiguredProjectMap.get(fileName); 3849 if (configFiles) { 3850 for (const configFile of configFiles) { 3851 this.closeConfiguredProjectReferencedFromExternalProject(configFile); 3852 } 3853 this.externalProjectToConfiguredProjectMap.delete(fileName); 3854 } 3855 else { 3856 // close external project 3857 const externalProject = this.findExternalProjectByProjectName(uncheckedFileName); 3858 if (externalProject) { 3859 this.removeProject(externalProject); 3860 } 3861 } 3862 } 3863 3864 openExternalProjects(projects: protocol.ExternalProject[]): void { 3865 // record project list before the update 3866 const projectsToClose = arrayToMap(this.externalProjects, p => p.getProjectName(), _ => true); 3867 forEachKey(this.externalProjectToConfiguredProjectMap, externalProjectName => { 3868 projectsToClose.set(externalProjectName, true); 3869 }); 3870 3871 for (const externalProject of projects) { 3872 this.openExternalProject(externalProject); 3873 // delete project that is present in input list 3874 projectsToClose.delete(externalProject.projectFileName); 3875 } 3876 3877 // close projects that were missing in the input list 3878 forEachKey(projectsToClose, externalProjectName => { 3879 this.closeExternalProject(externalProjectName); 3880 }); 3881 } 3882 3883 /** Makes a filename safe to insert in a RegExp */ 3884 private static readonly filenameEscapeRegexp = /[-\/\\^$*+?.()|[\]{}]/g; 3885 private static escapeFilenameForRegex(filename: string) { 3886 return filename.replace(this.filenameEscapeRegexp, "\\$&"); 3887 } 3888 3889 resetSafeList(): void { 3890 this.safelist = defaultTypeSafeList; 3891 } 3892 3893 applySafeList(proj: protocol.ExternalProject): NormalizedPath[] { 3894 const { rootFiles } = proj; 3895 const typeAcquisition = proj.typeAcquisition!; 3896 Debug.assert(!!typeAcquisition, "proj.typeAcquisition should be set by now"); 3897 3898 if (typeAcquisition.enable === false || typeAcquisition.disableFilenameBasedTypeAcquisition) { 3899 return []; 3900 } 3901 3902 const typeAcqInclude = typeAcquisition.include || (typeAcquisition.include = []); 3903 const excludeRules: string[] = []; 3904 3905 const normalizedNames = rootFiles.map(f => normalizeSlashes(f.fileName)) as NormalizedPath[]; 3906 const excludedFiles: NormalizedPath[] = []; 3907 3908 for (const name of Object.keys(this.safelist)) { 3909 const rule = this.safelist[name]; 3910 for (const root of normalizedNames) { 3911 if (rule.match.test(root)) { 3912 this.logger.info(`Excluding files based on rule ${name} matching file '${root}'`); 3913 3914 // If the file matches, collect its types packages and exclude rules 3915 if (rule.types) { 3916 for (const type of rule.types) { 3917 // Best-effort de-duping here - doesn't need to be unduplicated but 3918 // we don't want the list to become a 400-element array of just 'kendo' 3919 if (typeAcqInclude.indexOf(type) < 0) { 3920 typeAcqInclude.push(type); 3921 } 3922 } 3923 } 3924 3925 if (rule.exclude) { 3926 for (const exclude of rule.exclude) { 3927 const processedRule = root.replace(rule.match, (...groups: string[]) => { 3928 return exclude.map(groupNumberOrString => { 3929 // RegExp group numbers are 1-based, but the first element in groups 3930 // is actually the original string, so it all works out in the end. 3931 if (typeof groupNumberOrString === "number") { 3932 if (!isString(groups[groupNumberOrString])) { 3933 // Specification was wrong - exclude nothing! 3934 this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`); 3935 // * can't appear in a filename; escape it because it's feeding into a RegExp 3936 return "\\*"; 3937 } 3938 return ProjectService.escapeFilenameForRegex(groups[groupNumberOrString]); 3939 } 3940 return groupNumberOrString; 3941 }).join(""); 3942 }); 3943 3944 if (excludeRules.indexOf(processedRule) === -1) { 3945 excludeRules.push(processedRule); 3946 } 3947 } 3948 } 3949 else { 3950 // If not rules listed, add the default rule to exclude the matched file 3951 const escaped = ProjectService.escapeFilenameForRegex(root); 3952 if (excludeRules.indexOf(escaped) < 0) { 3953 excludeRules.push(escaped); 3954 } 3955 } 3956 } 3957 } 3958 } 3959 3960 const excludeRegexes = excludeRules.map(e => new RegExp(e, "i")); 3961 const filesToKeep: protocol.ExternalFile[] = []; 3962 for (let i = 0; i < proj.rootFiles.length; i++) { 3963 if (excludeRegexes.some(re => re.test(normalizedNames[i]))) { 3964 excludedFiles.push(normalizedNames[i]); 3965 } 3966 else { 3967 let exclude = false; 3968 if (typeAcquisition.enable || typeAcquisition.enableAutoDiscovery) { 3969 const baseName = getBaseFileName(toFileNameLowerCase(normalizedNames[i])); 3970 if (fileExtensionIs(baseName, "js")) { 3971 const inferredTypingName = removeFileExtension(baseName); 3972 const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName); 3973 const typeName = this.legacySafelist.get(cleanedTypingName); 3974 if (typeName !== undefined) { 3975 this.logger.info(`Excluded '${normalizedNames[i]}' because it matched ${cleanedTypingName} from the legacy safelist`); 3976 excludedFiles.push(normalizedNames[i]); 3977 // *exclude* it from the project... 3978 exclude = true; 3979 // ... but *include* it in the list of types to acquire 3980 // Same best-effort dedupe as above 3981 if (typeAcqInclude.indexOf(typeName) < 0) { 3982 typeAcqInclude.push(typeName); 3983 } 3984 } 3985 } 3986 } 3987 if (!exclude) { 3988 // Exclude any minified files that get this far 3989 if (/^.+[\.-]min\.js$/.test(normalizedNames[i])) { 3990 excludedFiles.push(normalizedNames[i]); 3991 } 3992 else { 3993 filesToKeep.push(proj.rootFiles[i]); 3994 } 3995 } 3996 } 3997 } 3998 proj.rootFiles = filesToKeep; 3999 return excludedFiles; 4000 } 4001 4002 openExternalProject(proj: protocol.ExternalProject): void { 4003 // typingOptions has been deprecated and is only supported for backward compatibility 4004 // purposes. It should be removed in future releases - use typeAcquisition instead. 4005 if (proj.typingOptions && !proj.typeAcquisition) { 4006 const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions); 4007 proj.typeAcquisition = typeAcquisition; 4008 } 4009 proj.typeAcquisition = proj.typeAcquisition || {}; 4010 proj.typeAcquisition.include = proj.typeAcquisition.include || []; 4011 proj.typeAcquisition.exclude = proj.typeAcquisition.exclude || []; 4012 if (proj.typeAcquisition.enable === undefined) { 4013 proj.typeAcquisition.enable = hasNoTypeScriptSource(proj.rootFiles.map(f => f.fileName)); 4014 } 4015 4016 const excludedFiles = this.applySafeList(proj); 4017 4018 let tsConfigFiles: NormalizedPath[] | undefined; 4019 const rootFiles: protocol.ExternalFile[] = []; 4020 for (const file of proj.rootFiles) { 4021 const normalized = toNormalizedPath(file.fileName); 4022 if (getBaseConfigFileName(normalized)) { 4023 if (this.serverMode === LanguageServiceMode.Semantic && this.host.fileExists(normalized)) { 4024 (tsConfigFiles || (tsConfigFiles = [])).push(normalized); 4025 } 4026 } 4027 else { 4028 rootFiles.push(file); 4029 } 4030 } 4031 4032 // sort config files to simplify comparison later 4033 if (tsConfigFiles) { 4034 tsConfigFiles.sort(); 4035 } 4036 4037 const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); 4038 let exisingConfigFiles: string[] | undefined; 4039 if (externalProject) { 4040 externalProject.excludedFiles = excludedFiles; 4041 if (!tsConfigFiles) { 4042 const compilerOptions = convertCompilerOptions(proj.options); 4043 const watchOptionsAndErrors = convertWatchOptions(proj.options, externalProject.getCurrentDirectory()); 4044 const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(proj.projectFileName, compilerOptions, proj.rootFiles, externalFilePropertyReader); 4045 if (lastFileExceededProgramSize) { 4046 externalProject.disableLanguageService(lastFileExceededProgramSize); 4047 } 4048 else { 4049 externalProject.enableLanguageService(); 4050 } 4051 externalProject.setProjectErrors(watchOptionsAndErrors?.errors); 4052 // external project already exists and not config files were added - update the project and return; 4053 // The graph update here isnt postponed since any file open operation needs all updated external projects 4054 this.updateRootAndOptionsOfNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, watchOptionsAndErrors?.watchOptions); 4055 externalProject.updateGraph(); 4056 return; 4057 } 4058 // some config files were added to external project (that previously were not there) 4059 // close existing project and later we'll open a set of configured projects for these files 4060 this.closeExternalProject(proj.projectFileName); 4061 } 4062 else if (this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)) { 4063 // this project used to include config files 4064 if (!tsConfigFiles) { 4065 // config files were removed from the project - close existing external project which in turn will close configured projects 4066 this.closeExternalProject(proj.projectFileName); 4067 } 4068 else { 4069 // project previously had some config files - compare them with new set of files and close all configured projects that correspond to unused files 4070 const oldConfigFiles = this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)!; 4071 let iNew = 0; 4072 let iOld = 0; 4073 while (iNew < tsConfigFiles.length && iOld < oldConfigFiles.length) { 4074 const newConfig = tsConfigFiles[iNew]; 4075 const oldConfig = oldConfigFiles[iOld]; 4076 if (oldConfig < newConfig) { 4077 this.closeConfiguredProjectReferencedFromExternalProject(oldConfig); 4078 iOld++; 4079 } 4080 else if (oldConfig > newConfig) { 4081 iNew++; 4082 } 4083 else { 4084 // record existing config files so avoid extra add-refs 4085 (exisingConfigFiles || (exisingConfigFiles = [])).push(oldConfig); 4086 iOld++; 4087 iNew++; 4088 } 4089 } 4090 for (let i = iOld; i < oldConfigFiles.length; i++) { 4091 // projects for all remaining old config files should be closed 4092 this.closeConfiguredProjectReferencedFromExternalProject(oldConfigFiles[i]); 4093 } 4094 } 4095 } 4096 if (tsConfigFiles) { 4097 // store the list of tsconfig files that belong to the external project 4098 this.externalProjectToConfiguredProjectMap.set(proj.projectFileName, tsConfigFiles); 4099 for (const tsconfigFile of tsConfigFiles) { 4100 let project = this.findConfiguredProjectByProjectName(tsconfigFile); 4101 if (!project) { 4102 // errors are stored in the project, do not need to update the graph 4103 project = this.getHostPreferences().lazyConfiguredProjectsFromExternalProject ? 4104 this.createConfiguredProjectWithDelayLoad(tsconfigFile, `Creating configured project in external project: ${proj.projectFileName}`) : 4105 this.createLoadAndUpdateConfiguredProject(tsconfigFile, `Creating configured project in external project: ${proj.projectFileName}`); 4106 } 4107 if (project && !contains(exisingConfigFiles, tsconfigFile)) { 4108 // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project 4109 project.addExternalProjectReference(); 4110 } 4111 } 4112 } 4113 else { 4114 // no config files - remove the item from the collection 4115 // Create external project and update its graph, do not delay update since 4116 // any file open operation needs all updated external projects 4117 this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName); 4118 const project = this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles); 4119 project.updateGraph(); 4120 } 4121 } 4122 4123 hasDeferredExtension() { 4124 for (const extension of this.hostConfiguration.extraFileExtensions!) { // TODO: GH#18217 4125 if (extension.scriptKind === ScriptKind.Deferred) { 4126 return true; 4127 } 4128 } 4129 4130 return false; 4131 } 4132 4133 /** @internal */ 4134 requestEnablePlugin(project: Project, pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | undefined) { 4135 if (!this.host.importPlugin && !this.host.require) { 4136 this.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded"); 4137 return; 4138 } 4139 4140 this.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`); 4141 if (!pluginConfigEntry.name || parsePackageName(pluginConfigEntry.name).rest) { 4142 this.logger.info(`Skipped loading plugin ${pluginConfigEntry.name || JSON.stringify(pluginConfigEntry)} because only package name is allowed plugin name`); 4143 return; 4144 } 4145 4146 // If the host supports dynamic import, begin enabling the plugin asynchronously. 4147 if (this.host.importPlugin) { 4148 const importPromise = project.beginEnablePluginAsync(pluginConfigEntry, searchPaths, pluginConfigOverrides); 4149 this.pendingPluginEnablements ??= new Map(); 4150 let promises = this.pendingPluginEnablements.get(project); 4151 if (!promises) this.pendingPluginEnablements.set(project, promises = []); 4152 promises.push(importPromise); 4153 return; 4154 } 4155 4156 // Otherwise, load the plugin using `require` 4157 project.endEnablePlugin(project.beginEnablePluginSync(pluginConfigEntry, searchPaths, pluginConfigOverrides)); 4158 } 4159 4160 /** @internal */ 4161 hasNewPluginEnablementRequests() { 4162 return !!this.pendingPluginEnablements; 4163 } 4164 4165 /** @internal */ 4166 hasPendingPluginEnablements() { 4167 return !!this.currentPluginEnablementPromise; 4168 } 4169 4170 /** 4171 * Waits for any ongoing plugin enablement requests to complete. 4172 * 4173 * @internal 4174 */ 4175 async waitForPendingPlugins() { 4176 while (this.currentPluginEnablementPromise) { 4177 await this.currentPluginEnablementPromise; 4178 } 4179 } 4180 4181 /** 4182 * Starts enabling any requested plugins without waiting for the result. 4183 * 4184 * @internal 4185 */ 4186 enableRequestedPlugins() { 4187 if (this.pendingPluginEnablements) { 4188 void this.enableRequestedPluginsAsync(); 4189 } 4190 } 4191 4192 private async enableRequestedPluginsAsync() { 4193 if (this.currentPluginEnablementPromise) { 4194 // If we're already enabling plugins, wait for any existing operations to complete 4195 await this.waitForPendingPlugins(); 4196 } 4197 4198 // Skip if there are no new plugin enablement requests 4199 if (!this.pendingPluginEnablements) { 4200 return; 4201 } 4202 4203 // Consume the pending plugin enablement requests 4204 const entries = arrayFrom(this.pendingPluginEnablements.entries()); 4205 this.pendingPluginEnablements = undefined; 4206 4207 // Start processing the requests, keeping track of the promise for the operation so that 4208 // project consumers can potentially wait for the plugins to load. 4209 this.currentPluginEnablementPromise = this.enableRequestedPluginsWorker(entries); 4210 await this.currentPluginEnablementPromise; 4211 } 4212 4213 private async enableRequestedPluginsWorker(pendingPlugins: [Project, Promise<BeginEnablePluginResult>[]][]) { 4214 // This should only be called from `enableRequestedPluginsAsync`, which ensures this precondition is met. 4215 Debug.assert(this.currentPluginEnablementPromise === undefined); 4216 4217 // Process all pending plugins, partitioned by project. This way a project with few plugins doesn't need to wait 4218 // on a project with many plugins. 4219 await Promise.all(map(pendingPlugins, ([project, promises]) => this.enableRequestedPluginsForProjectAsync(project, promises))); 4220 4221 // Clear the pending operation and notify the client that projects have been updated. 4222 this.currentPluginEnablementPromise = undefined; 4223 this.sendProjectsUpdatedInBackgroundEvent(); 4224 } 4225 4226 private async enableRequestedPluginsForProjectAsync(project: Project, promises: Promise<BeginEnablePluginResult>[]) { 4227 // Await all pending plugin imports. This ensures all requested plugin modules are fully loaded 4228 // prior to patching the language service, and that any promise rejections are observed. 4229 const results = await Promise.all(promises); 4230 if (project.isClosed()) { 4231 // project is not alive, so don't enable plugins. 4232 return; 4233 } 4234 4235 for (const result of results) { 4236 project.endEnablePlugin(result); 4237 } 4238 4239 // Plugins may have modified external files, so mark the project as dirty. 4240 this.delayUpdateProjectGraph(project); 4241 } 4242 4243 configurePlugin(args: protocol.ConfigurePluginRequestArguments) { 4244 // For any projects that already have the plugin loaded, configure the plugin 4245 this.forEachEnabledProject(project => project.onPluginConfigurationChanged(args.pluginName, args.configuration)); 4246 4247 // Also save the current configuration to pass on to any projects that are yet to be loaded. 4248 // If a plugin is configured twice, only the latest configuration will be remembered. 4249 this.currentPluginConfigOverrides = this.currentPluginConfigOverrides || new Map(); 4250 this.currentPluginConfigOverrides.set(args.pluginName, args.configuration); 4251 } 4252 4253 /** @internal */ 4254 getPackageJsonsVisibleToFile(fileName: string, rootDir?: string): readonly ProjectPackageJsonInfo[] { 4255 const packageJsonCache = this.packageJsonCache; 4256 const rootPath = rootDir && this.toPath(rootDir); 4257 const filePath = this.toPath(fileName); 4258 const result: ProjectPackageJsonInfo[] = []; 4259 const processDirectory = (directory: Path): boolean | undefined => { 4260 switch (packageJsonCache.directoryHasPackageJson(directory)) { 4261 // Sync and check same directory again 4262 case Ternary.Maybe: 4263 packageJsonCache.searchDirectoryAndAncestors(directory); 4264 return processDirectory(directory); 4265 // Check package.json 4266 case Ternary.True: 4267 const packageJsonFileName = combinePaths(directory, "package.json"); 4268 this.watchPackageJsonFile(packageJsonFileName as Path); 4269 const info = packageJsonCache.getInDirectory(directory); 4270 if (info) result.push(info); 4271 } 4272 if (rootPath && rootPath === directory) { 4273 return true; 4274 } 4275 }; 4276 4277 forEachAncestorDirectory(getDirectoryPath(filePath), processDirectory); 4278 return result; 4279 } 4280 4281 /** @internal */ 4282 getNearestAncestorDirectoryWithPackageJson(fileName: string): string | undefined { 4283 return forEachAncestorDirectory(fileName, directory => { 4284 switch (this.packageJsonCache.directoryHasPackageJson(this.toPath(directory))) { 4285 case Ternary.True: return directory; 4286 case Ternary.False: return undefined; 4287 case Ternary.Maybe: 4288 return this.host.fileExists(combinePaths(directory, "package.json")) 4289 ? directory 4290 : undefined; 4291 } 4292 }); 4293 } 4294 4295 /** @internal */ 4296 private watchPackageJsonFile(path: Path) { 4297 const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = new Map()); 4298 if (!watchers.has(path)) { 4299 this.invalidateProjectPackageJson(path); 4300 watchers.set(path, this.watchFactory.watchFile( 4301 path, 4302 (fileName, eventKind) => { 4303 const path = this.toPath(fileName); 4304 switch (eventKind) { 4305 case FileWatcherEventKind.Created: 4306 return Debug.fail(); 4307 case FileWatcherEventKind.Changed: 4308 this.packageJsonCache.addOrUpdate(path); 4309 this.invalidateProjectPackageJson(path); 4310 break; 4311 case FileWatcherEventKind.Deleted: 4312 this.packageJsonCache.delete(path); 4313 this.invalidateProjectPackageJson(path); 4314 watchers.get(path)!.close(); 4315 watchers.delete(path); 4316 } 4317 }, 4318 PollingInterval.Low, 4319 this.hostConfiguration.watchOptions, 4320 WatchType.PackageJson, 4321 )); 4322 } 4323 } 4324 4325 /** @internal */ 4326 private onAddPackageJson(path: Path) { 4327 this.packageJsonCache.addOrUpdate(path); 4328 this.watchPackageJsonFile(path); 4329 } 4330 4331 /** @internal */ 4332 includePackageJsonAutoImports(): PackageJsonAutoImportPreference { 4333 switch (this.hostConfiguration.preferences.includePackageJsonAutoImports) { 4334 case "on": return PackageJsonAutoImportPreference.On; 4335 case "off": return PackageJsonAutoImportPreference.Off; 4336 default: return PackageJsonAutoImportPreference.Auto; 4337 } 4338 } 4339 4340 /** @internal */ 4341 private invalidateProjectPackageJson(packageJsonPath: Path | undefined) { 4342 this.configuredProjects.forEach(invalidate); 4343 this.inferredProjects.forEach(invalidate); 4344 this.externalProjects.forEach(invalidate); 4345 function invalidate(project: Project) { 4346 if (packageJsonPath) { 4347 project.onPackageJsonChange(packageJsonPath); 4348 } 4349 else { 4350 project.onAutoImportProviderSettingsChanged(); 4351 } 4352 } 4353 } 4354 4355 /** @internal */ 4356 getIncompleteCompletionsCache() { 4357 return this.incompleteCompletionsCache ||= createIncompleteCompletionsCache(); 4358 } 4359} 4360 4361function createIncompleteCompletionsCache(): IncompleteCompletionsCache { 4362 let info: CompletionInfo | undefined; 4363 return { 4364 get() { 4365 return info; 4366 }, 4367 set(newInfo) { 4368 info = newInfo; 4369 }, 4370 clear() { 4371 info = undefined; 4372 } 4373 }; 4374} 4375 4376/** @internal */ 4377export type ScriptInfoOrConfig = ScriptInfo | TsConfigSourceFile; 4378/** @internal */ 4379export function isConfigFile(config: ScriptInfoOrConfig): config is TsConfigSourceFile { 4380 return (config as TsConfigSourceFile).kind !== undefined; 4381} 4382 4383function printProjectWithoutFileNames(project: Project) { 4384 project.print(/*writeProjectFileNames*/ false); 4385} 4386