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