diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 2b62da72d24..27bd1fd3d3b 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -813,6 +813,8 @@ namespace ts { let resolvedProjectReferences: ReadonlyArray | undefined; let projectReferenceRedirects: Map | undefined; let mapFromFileToProjectReferenceRedirects: Map | undefined; + let mapFromToProjectReferenceRedirectSource: Map | undefined; + const useSourceOfReference = host.useSourceInsteadOfReferenceRedirect && host.useSourceInsteadOfReferenceRedirect(); const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); @@ -824,17 +826,29 @@ namespace ts { if (!resolvedProjectReferences) { resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); } + if (host.setGetSourceOfProjectReferenceRedirect) { + host.setGetSourceOfProjectReferenceRedirect(getSourceOfProjectReferenceRedirect); + } if (rootNames.length) { for (const parsedRef of resolvedProjectReferences) { if (!parsedRef) continue; const out = parsedRef.commandLine.options.outFile || parsedRef.commandLine.options.out; - if (out) { - processSourceFile(changeExtension(out, ".d.ts"), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + if (useSourceOfReference) { + if (out || getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { + for (const fileName of parsedRef.commandLine.fileNames) { + processSourceFile(fileName, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + } } - else if (getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { - for (const fileName of parsedRef.commandLine.fileNames) { - if (!fileExtensionIs(fileName, Extension.Dts) && hasTSFileExtension(fileName)) { - processSourceFile(getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames()), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + else { + if (out) { + processSourceFile(changeExtension(out, ".d.ts"), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + else if (getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { + for (const fileName of parsedRef.commandLine.fileNames) { + if (!fileExtensionIs(fileName, Extension.Dts) && hasTSFileExtension(fileName)) { + processSourceFile(getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames()), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } } } } @@ -1212,6 +1226,9 @@ namespace ts { } if (projectReferences) { resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + if (host.setGetSourceOfProjectReferenceRedirect) { + host.setGetSourceOfProjectReferenceRedirect(getSourceOfProjectReferenceRedirect); + } } // check if program source files has changed in the way that can affect structure of the program @@ -2220,6 +2237,14 @@ namespace ts { // Get source file from normalized fileName function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, refFile: SourceFile, refPos: number, refEnd: number, packageId: PackageId | undefined): SourceFile | undefined { + if (useSourceOfReference) { + const source = getSourceOfProjectReferenceRedirect(fileName); + if (source) { + return isString(source) ? + findSourceFile(source, toPath(source), isDefaultLib, ignoreNoDefaultLib, refFile, refPos, refEnd, packageId) : + undefined; + } + } const originalFileName = fileName; if (filesByName.has(path)) { const file = filesByName.get(path); @@ -2267,7 +2292,7 @@ namespace ts { } let redirectedPath: Path | undefined; - if (refFile) { + if (refFile && !useSourceOfReference) { const redirectProject = getProjectReferenceRedirectProject(fileName); if (redirectProject) { if (redirectProject.commandLine.options.outFile || redirectProject.commandLine.options.out) { @@ -2286,15 +2311,20 @@ namespace ts { } // We haven't looked for this file, do so now and cache result - const file = host.getSourceFile(fileName, options.target!, hostErrorMessage => { // TODO: GH#18217 - if (refFile !== undefined && refPos !== undefined && refEnd !== undefined) { - fileProcessingDiagnostics.add(createFileDiagnostic(refFile, refPos, refEnd - refPos, - Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage)); - } - else { - fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage)); - } - }, shouldCreateNewSourceFile); + const file = host.getSourceFile( + fileName, + options.target!, + hostErrorMessage => { // TODO: GH#18217 + if (refFile !== undefined && refPos !== undefined && refEnd !== undefined) { + fileProcessingDiagnostics.add(createFileDiagnostic(refFile, refPos, refEnd - refPos, + Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage)); + } + else { + fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage)); + } + }, + shouldCreateNewSourceFile + ); if (packageId) { const packageIdKey = packageIdToString(packageId); @@ -2424,6 +2454,30 @@ namespace ts { }); } + function getSourceOfProjectReferenceRedirect(file: string) { + if (!isDeclarationFileName(file)) return undefined; + if (mapFromToProjectReferenceRedirectSource === undefined) { + mapFromToProjectReferenceRedirectSource = createMap(); + forEachResolvedProjectReference(resolvedRef => { + if (resolvedRef) { + const out = resolvedRef.commandLine.options.outFile || resolvedRef.commandLine.options.out; + if (out) { + // Dont know which source file it means so return true? + const outputDts = changeExtension(out, Extension.Dts); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); + } + else { + forEach(resolvedRef.commandLine.fileNames, fileName => { + const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, host.useCaseSensitiveFileNames()); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); + }); + } + } + }); + } + return mapFromToProjectReferenceRedirectSource.get(toPath(file)); + } + function forEachProjectReference( projectReferences: ReadonlyArray | undefined, resolvedProjectReferences: ReadonlyArray | undefined, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ec46da039d9..fc24088e0fa 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5166,11 +5166,20 @@ namespace ts { /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; + /* @internal */ setGetSourceOfProjectReferenceRedirect?(getSource: GetSourceOfProjectReferenceRedirect): void; + /* @internal */ useSourceInsteadOfReferenceRedirect?(): boolean; // TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesn't use compilerHost as base /*@internal*/createDirectory?(directory: string): void; } + /** true if --out otherwise source file name */ + /*@internal*/ + export type SourceOfProjectReferenceRedirect = string | true ; + + /*@internal*/ + export type GetSourceOfProjectReferenceRedirect = (fileName: string) => SourceOfProjectReferenceRedirect | undefined; + /* @internal */ export const enum TransformFlags { None = 0, diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 4e5435feaba..f7fb537f543 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1798,7 +1798,7 @@ namespace ts.server { let scriptInfo: ScriptInfo | NormalizedPath; let path: Path; // Use the project's fileExists so that it can use caching instead of reaching to disk for the query - if (!isDynamic && !project.fileExists(newRootFile)) { + if (!isDynamic && !project.fileExistsWithCache(newRootFile)) { path = normalizedPathToPath(normalizedPath, this.currentDirectory, this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path)!; if (isScriptInfo(existingValue)) { @@ -1831,7 +1831,7 @@ namespace ts.server { projectRootFilesMap.forEach((value, path) => { if (!newRootScriptInfoMap.has(path)) { if (isScriptInfo(value)) { - project.removeFile(value, project.fileExists(path), /*detachFromProject*/ true); + project.removeFile(value, project.fileExistsWithCache(path), /*detachFromProject*/ true); } else { projectRootFilesMap.delete(path); diff --git a/src/server/project.ts b/src/server/project.ts index d1605e22d1d..3e69a785516 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -381,6 +381,11 @@ namespace ts.server { } fileExists(file: string): boolean { + return this.fileExistsWithCache(file); + } + + /* @internal */ + fileExistsWithCache(file: string): boolean { // As an optimization, don't hit the disks for files we already know don't exist // (because we're watching for their creation). const path = this.toPath(file); @@ -1369,6 +1374,7 @@ namespace ts.server { configFileWatcher: FileWatcher | undefined; private directoriesWatchedForWildcards: Map | undefined; readonly canonicalConfigFilePath: NormalizedPath; + private getSourceOfProjectReferenceRedirect: GetSourceOfProjectReferenceRedirect | undefined; /* @internal */ pendingReload: ConfigFileProgramReloadLevel | undefined; @@ -1414,6 +1420,25 @@ namespace ts.server { this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); } + /* @internal */ + setGetSourceOfProjectReferenceRedirect(getSource: GetSourceOfProjectReferenceRedirect) { + this.getSourceOfProjectReferenceRedirect = getSource; + } + + /* @internal */ + useSourceInsteadOfReferenceRedirect() { + return true; + } + + fileExists(file: string): boolean { + // Project references go to source file instead of .d.ts file + if (this.getSourceOfProjectReferenceRedirect) { + const source = this.getSourceOfProjectReferenceRedirect(file); + if (source) return isString(source) ? super.fileExists(source) : true; + } + return super.fileExists(file); + } + /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise. @@ -1436,6 +1461,7 @@ namespace ts.server { default: result = super.updateGraph(); } + this.getSourceOfProjectReferenceRedirect = undefined; this.projectService.sendProjectLoadingFinishEvent(this); this.projectService.sendProjectTelemetry(this); return result; diff --git a/src/services/services.ts b/src/services/services.ts index fab6f88b779..a8b05402e76 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1245,6 +1245,12 @@ namespace ts { return host.resolveTypeReferenceDirectives!(typeReferenceDirectiveNames, containingFile, redirectedReference); }; } + if (host.setGetSourceOfProjectReferenceRedirect) { + compilerHost.setGetSourceOfProjectReferenceRedirect = getSource => host.setGetSourceOfProjectReferenceRedirect!(getSource); + } + if (host.useSourceInsteadOfReferenceRedirect) { + compilerHost.useSourceInsteadOfReferenceRedirect = () => host.useSourceInsteadOfReferenceRedirect!(); + } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); const options: CreateProgramOptions = { diff --git a/src/services/types.ts b/src/services/types.ts index b97125734f7..3c9509ca5e9 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -236,6 +236,10 @@ namespace ts { getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; /* @internal */ getSourceFileLike?(fileName: string): SourceFileLike | undefined; + /* @internal */ + setGetSourceOfProjectReferenceRedirect?(getSource: GetSourceOfProjectReferenceRedirect): void; + /* @internal */ + useSourceInsteadOfReferenceRedirect?(): boolean; } /* @internal */ diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 82ab70b6e6f..53459d993cf 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8546,11 +8546,13 @@ declare namespace ts.server { private typeAcquisition; private directoriesWatchedForWildcards; readonly canonicalConfigFilePath: NormalizedPath; + private getSourceOfProjectReferenceRedirect; /** Ref count to the project when opened from external project */ private externalProjectRefCount; private projectErrors; private projectReferences; protected isInitialLoadPending: () => boolean; + fileExists(file: string): boolean; /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise.