From 3f5e0805ec94f6a9e36e599bd8411ab39bcfa693 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 11 Mar 2020 12:51:57 -0700 Subject: [PATCH] Move useSourceOfProjectReferenceRedirect to program so other hosts can use it too, enabling it for WatchHost --- src/compiler/program.ts | 202 ++++++++++++++++-- src/compiler/resolutionCache.ts | 3 +- src/compiler/types.ts | 1 - src/compiler/watchPublic.ts | 4 + src/server/editorServices.ts | 4 +- src/server/project.ts | 187 ++-------------- src/services/services.ts | 4 +- src/services/types.ts | 2 +- src/testRunner/tsconfig.json | 3 +- src/testRunner/unittests/tscWatch/helpers.ts | 2 +- .../sourceOfProjectReferenceRedirect.ts | 158 ++++++++++++++ .../reference/api/tsserverlibrary.d.ts | 29 +-- tests/baselines/reference/api/typescript.d.ts | 2 + 13 files changed, 378 insertions(+), 223 deletions(-) create mode 100644 src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts diff --git a/src/compiler/program.ts b/src/compiler/program.ts index d876626119a33..09446ec4d8286 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -806,7 +806,17 @@ namespace ts { let projectReferenceRedirects: Map | undefined; let mapFromFileToProjectReferenceRedirects: Map | undefined; let mapFromToProjectReferenceRedirectSource: Map | undefined; - const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect && host.useSourceOfProjectReferenceRedirect(); + + const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() && + !options.disableSourceOfProjectReferenceRedirect; + const onProgramCreateComplete = updateHostForUseSourceOfProjectReferenceRedirect({ + compilerHost: host, + useSourceOfProjectReferenceRedirect, + toPath, + getResolvedProjectReferences, + getSourceOfProjectReferenceRedirect, + forEachResolvedProjectReference + }); const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); // We set `structuralIsReused` to `undefined` because `tryReuseStructureFromOldProgram` calls `tryReuseStructureFromOldProgram` which checks @@ -821,12 +831,6 @@ namespace ts { if (!resolvedProjectReferences) { resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); } - if (host.setResolvedProjectReferenceCallbacks) { - host.setResolvedProjectReferenceCallbacks({ - getSourceOfProjectReferenceRedirect, - forEachResolvedProjectReference - }); - } if (rootNames.length) { for (const parsedRef of resolvedProjectReferences) { if (!parsedRef) continue; @@ -970,6 +974,7 @@ namespace ts { useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), }; + onProgramCreateComplete(); verifyCompilerOptions(); performance.mark("afterProgram"); performance.measure("Program", "beforeProgram", "afterProgram"); @@ -1248,12 +1253,6 @@ namespace ts { } if (projectReferences) { resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); - if (host.setResolvedProjectReferenceCallbacks) { - host.setResolvedProjectReferenceCallbacks({ - getSourceOfProjectReferenceRedirect, - forEachResolvedProjectReference - }); - } } // check if program source files has changed in the way that can affect structure of the program @@ -3460,6 +3459,183 @@ namespace ts { } } + interface SymlinkedDirectory { + real: string; + realPath: Path; + } + + interface HostForUseSourceOfProjectReferenceRedirect { + compilerHost: CompilerHost; + useSourceOfProjectReferenceRedirect: boolean; + toPath(fileName: string): Path; + getResolvedProjectReferences(): readonly (ResolvedProjectReference | undefined)[] | undefined; + getSourceOfProjectReferenceRedirect(fileName: string): SourceOfProjectReferenceRedirect | undefined; + forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined; + } + + function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) { + let mapOfDeclarationDirectories: Map | undefined; + let symlinkedDirectories: Map | undefined; + let symlinkedFiles: Map | undefined; + + const originalFileExists = host.compilerHost.fileExists; + const originalDirectoryExists = host.compilerHost.directoryExists; + const originalGetDirectories = host.compilerHost.getDirectories; + const originalRealpath = host.compilerHost.realpath; + + + if (!host.useSourceOfProjectReferenceRedirect) return noop; + + // This implementation of fileExists checks if the file being requested is + // .d.ts file for the referenced Project. + // If it is it returns true irrespective of whether that file exists on host + host.compilerHost.fileExists = (file) => { + if (originalFileExists.call(host.compilerHost, file)) return true; + if (!host.getResolvedProjectReferences()) return false; + if (!isDeclarationFileName(file)) return false; + + // Project references go to source file instead of .d.ts file + return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true); + }; + + if (originalDirectoryExists) { + // This implementation of directoryExists checks if the directory being requested is + // directory of .d.ts file for the referenced Project. + // If it is it returns true irrespective of whether that directory exists on host + host.compilerHost.directoryExists = path => { + if (originalDirectoryExists.call(host.compilerHost, path)) { + handleDirectoryCouldBeSymlink(path); + return true; + } + + if (!host.getResolvedProjectReferences()) return false; + + if (!mapOfDeclarationDirectories) { + mapOfDeclarationDirectories = createMap(); + host.forEachResolvedProjectReference(ref => { + if (!ref) return; + const out = ref.commandLine.options.outFile || ref.commandLine.options.out; + if (out) { + mapOfDeclarationDirectories!.set(getDirectoryPath(host.toPath(out)), true); + } + else { + // Set declaration's in different locations only, if they are next to source the directory present doesnt change + const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir; + if (declarationDir) { + mapOfDeclarationDirectories!.set(host.toPath(declarationDir), true); + } + } + }); + } + + return fileOrDirectoryExistsUsingSource(path, /*isFile*/ false); + }; + } + + if (originalGetDirectories) { + // Call getDirectories only if directory actually present on the host + // This is needed to ensure that we arent getting directories that we fake about presence for + host.compilerHost.getDirectories = path => + !host.getResolvedProjectReferences() || (originalDirectoryExists && originalDirectoryExists.call(host.compilerHost, path)) ? + originalGetDirectories.call(host.compilerHost, path) : + []; + } + + // This is something we keep for life time of the host + if (originalRealpath) { + host.compilerHost.realpath = s => + symlinkedFiles?.get(host.toPath(s)) || + originalRealpath.call(host.compilerHost, s); + } + + return onProgramCreateComplete; + + + function onProgramCreateComplete() { + host.compilerHost.fileExists = originalFileExists; + host.compilerHost.directoryExists = originalDirectoryExists; + host.compilerHost.getDirectories = originalGetDirectories; + // DO not revert realpath as it could be used later + } + + function fileExistsIfProjectReferenceDts(file: string) { + const source = host.getSourceOfProjectReferenceRedirect(file); + return source !== undefined ? + isString(source) ? originalFileExists.call(host.compilerHost, source) : true : + undefined; + } + + function directoryExistsIfProjectReferenceDeclDir(dir: string) { + const dirPath = host.toPath(dir); + const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`; + return forEachKey( + mapOfDeclarationDirectories!, + declDirPath => dirPath === declDirPath || + // Any parent directory of declaration dir + startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) || + // Any directory inside declaration dir + startsWith(dirPath, `${declDirPath}/`) + ); + } + + function handleDirectoryCouldBeSymlink(directory: string) { + if (!host.getResolvedProjectReferences()) return; + + // Because we already watch node_modules, handle symlinks in there + if (!originalRealpath || !stringContains(directory, nodeModulesPathPart)) return; + if (!symlinkedDirectories) symlinkedDirectories = createMap(); + const directoryPath = ensureTrailingDirectorySeparator(host.toPath(directory)); + if (symlinkedDirectories.has(directoryPath)) return; + + const real = normalizePath(originalRealpath.call(host.compilerHost, directory)); + let realPath: Path; + if (real === directory || + (realPath = ensureTrailingDirectorySeparator(host.toPath(real))) === directoryPath) { + // not symlinked + symlinkedDirectories.set(directoryPath, false); + return; + } + + symlinkedDirectories.set(directoryPath, { + real: ensureTrailingDirectorySeparator(real), + realPath + }); + } + + function fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean { + const fileOrDirectoryExistsUsingSource = isFile ? + (file: string) => fileExistsIfProjectReferenceDts(file) : + (dir: string) => directoryExistsIfProjectReferenceDeclDir(dir); + // Check current directory or file + const result = fileOrDirectoryExistsUsingSource(fileOrDirectory); + if (result !== undefined) return result; + + if (!symlinkedDirectories) return false; + const fileOrDirectoryPath = host.toPath(fileOrDirectory); + if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false; + if (isFile && symlinkedFiles && symlinkedFiles.has(fileOrDirectoryPath)) return true; + + // If it contains node_modules check if its one of the symlinked path we know of + return firstDefinedIterator( + symlinkedDirectories.entries(), + ([directoryPath, symlinkedDirectory]) => { + if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined; + const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath)); + if (isFile && result) { + if (!symlinkedFiles) symlinkedFiles = createMap(); + // Store the real path for the file' + const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, host.compilerHost.getCurrentDirectory()); + symlinkedFiles.set( + fileOrDirectoryPath, + `${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}` + ); + } + return result; + } + ) || false; + } + } + /*@internal*/ export function handleNoEmitOptions(program: ProgramToEmitFilesAndReportErrors, sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): EmitResult | undefined { const options = program.getCompilerOptions(); diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index de26a3ea148f1..3b0695450e04c 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -57,6 +57,7 @@ namespace ts { writeLog(s: string): void; getCurrentProgram(): Program | undefined; fileIsOpen(filePath: Path): boolean; + getCompilerHost?(): CompilerHost | undefined; } interface DirectoryWatchesOfFailedLookup { @@ -364,7 +365,7 @@ namespace ts { resolution = resolutionInDirectory; } else { - resolution = loader(name, containingFile, compilerOptions, resolutionHost, redirectedReference); + resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference); perDirectoryResolution.set(name, resolution); } resolutionsInFile.set(name, resolution); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d351a440a842c..9ae1777851202 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5700,7 +5700,6 @@ namespace ts { /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; - /* @internal */ setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void; /* @internal */ useSourceOfProjectReferenceRedirect?(): 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 diff --git a/src/compiler/watchPublic.ts b/src/compiler/watchPublic.ts index 0850466f9fd72..f8aa32188bbcd 100644 --- a/src/compiler/watchPublic.ts +++ b/src/compiler/watchPublic.ts @@ -114,6 +114,9 @@ namespace ts { } export interface WatchCompilerHost extends ProgramHost, WatchHost { + /** Instead of using output d.ts file from project reference, use its source file */ + useSourceOfProjectReferenceRedirect?(): boolean; + /** If provided, callback to invoke after every new program creation */ afterProgramCreate?(program: T): void; } @@ -280,6 +283,7 @@ namespace ts { // Members for ResolutionCacheHost compilerHost.toPath = toPath; compilerHost.getCompilationSettings = () => compilerOptions; + compilerHost.useSourceOfProjectReferenceRedirect = maybeBind(host, host.useSourceOfProjectReferenceRedirect); compilerHost.watchDirectoryOfFailedLookupLocation = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.FailedLookupLocations); compilerHost.watchTypeRootsDirectory = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.TypeRoots); compilerHost.getCachedDirectoryStructureHost = () => cachedDirectoryStructureHost; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d2c0be2836f03..6caaefa10e4a7 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1986,7 +1986,7 @@ namespace ts.server { const isDynamic = isDynamicFileName(fileName); 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.fileExistsWithCache(newRootFile)) { + if (!isDynamic && !project.fileExists(newRootFile)) { path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path); if (existingValue) { @@ -2035,7 +2035,7 @@ namespace ts.server { projectRootFilesMap.forEach((value, path) => { if (!newRootScriptInfoMap.has(path)) { if (value.info) { - project.removeFile(value.info, project.fileExistsWithCache(path), /*detachFromProject*/ true); + project.removeFile(value.info, project.fileExists(path), /*detachFromProject*/ true); } else { projectRootFilesMap.delete(path); diff --git a/src/server/project.ts b/src/server/project.ts index afcc7145febe4..9eb9438f6f375 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -260,7 +260,7 @@ namespace ts.server { protected watchOptions: WatchOptions | undefined, directoryStructureHost: DirectoryStructureHost, currentDirectory: string | undefined, - customRealpath?: (s: string) => string) { + ) { this.directoryStructureHost = directoryStructureHost; this.currentDirectory = this.projectService.getNormalizedAbsolutePath(currentDirectory || ""); this.getCanonicalFileName = this.projectService.toCanonicalFileName; @@ -286,10 +286,7 @@ namespace ts.server { else if (host.trace) { this.trace = s => host.trace!(s); } - - if (host.realpath) { - this.realpath = customRealpath || (path => host.realpath!(path)); - } + this.realpath = maybeBind(host, host.realpath); // Use the current directory as resolution root only if the project created using current directory string this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory, /*logChangesWhenResolvingModule*/ true); @@ -427,11 +424,6 @@ 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); @@ -1746,12 +1738,6 @@ namespace ts.server { } } - /*@internal*/ - interface SymlinkedDirectory { - real: string; - realPath: Path; - } - /** * If a file is opened, the server will look for a tsconfig (or jsconfig) * and if successful create a ConfiguredProject for it. @@ -1763,10 +1749,6 @@ namespace ts.server { configFileWatcher: FileWatcher | undefined; private directoriesWatchedForWildcards: Map | undefined; readonly canonicalConfigFilePath: NormalizedPath; - private projectReferenceCallbacks: ResolvedProjectReferenceCallbacks | undefined; - private mapOfDeclarationDirectories: Map | undefined; - private symlinkedDirectories: Map | undefined; - private symlinkedFiles: Map | undefined; /* @internal */ pendingReload: ConfigFileProgramReloadLevel | undefined; @@ -1802,6 +1784,9 @@ namespace ts.server { /*@internal*/ sendLoadingProjectFinish = false; + /*@internal*/ + private compilerHost?: CompilerHost; + /*@internal*/ constructor(configFileName: NormalizedPath, projectService: ProjectService, @@ -1818,161 +1803,23 @@ namespace ts.server { /*watchOptions*/ undefined, cachedDirectoryStructureHost, getDirectoryPath(configFileName), - projectService.host.realpath && (s => this.getRealpath(s)) ); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); } /* @internal */ - setResolvedProjectReferenceCallbacks(projectReferenceCallbacks: ResolvedProjectReferenceCallbacks) { - this.projectReferenceCallbacks = projectReferenceCallbacks; + setCompilerHost(host: CompilerHost) { + this.compilerHost = host; } /* @internal */ - useSourceOfProjectReferenceRedirect = () => !!this.languageServiceEnabled && - !this.getCompilerOptions().disableSourceOfProjectReferenceRedirect; - - private fileExistsIfProjectReferenceDts(file: string) { - const source = this.projectReferenceCallbacks!.getSourceOfProjectReferenceRedirect(file); - return source !== undefined ? - isString(source) ? super.fileExists(source) : true : - undefined; - } - - /** - * This implementation of fileExists checks if the file being requested is - * .d.ts file for the referenced Project. - * If it is it returns true irrespective of whether that file exists on host - */ - fileExists(file: string): boolean { - if (super.fileExists(file)) return true; - if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false; - if (!isDeclarationFileName(file)) return false; - - // Project references go to source file instead of .d.ts file - return this.fileOrDirectoryExistsUsingSource(file, /*isFile*/ true); - } - - private directoryExistsIfProjectReferenceDeclDir(dir: string) { - const dirPath = this.toPath(dir); - const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`; - return forEachKey( - this.mapOfDeclarationDirectories!, - declDirPath => dirPath === declDirPath || - // Any parent directory of declaration dir - startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) || - // Any directory inside declaration dir - startsWith(dirPath, `${declDirPath}/`) - ); - } - - /** - * This implementation of directoryExists checks if the directory being requested is - * directory of .d.ts file for the referenced Project. - * If it is it returns true irrespective of whether that directory exists on host - */ - directoryExists(path: string): boolean { - if (super.directoryExists(path)) { - this.handleDirectoryCouldBeSymlink(path); - return true; - } - if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false; - - if (!this.mapOfDeclarationDirectories) { - this.mapOfDeclarationDirectories = createMap(); - this.projectReferenceCallbacks.forEachResolvedProjectReference(ref => { - if (!ref) return; - const out = ref.commandLine.options.outFile || ref.commandLine.options.out; - if (out) { - this.mapOfDeclarationDirectories!.set(getDirectoryPath(this.toPath(out)), true); - } - else { - // Set declaration's in different locations only, if they are next to source the directory present doesnt change - const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir; - if (declarationDir) { - this.mapOfDeclarationDirectories!.set(this.toPath(declarationDir), true); - } - } - }); - } - - return this.fileOrDirectoryExistsUsingSource(path, /*isFile*/ false); - } - - /** - * Call super.getDirectories only if directory actually present on the host - * This is needed to ensure that we arent getting directories that we fake about presence for - */ - getDirectories(path: string): string[] { - return !this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks || super.directoryExists(path) ? - super.getDirectories(path) : - []; - } - - private realpathIfSymlinkedProjectReferenceDts(s: string): string | undefined { - return this.symlinkedFiles && this.symlinkedFiles.get(this.toPath(s)); - } - - private getRealpath(s: string): string { - return this.realpathIfSymlinkedProjectReferenceDts(s) || - this.projectService.host.realpath!(s); + getCompilerHost(): CompilerHost | undefined { + return this.compilerHost; } - private handleDirectoryCouldBeSymlink(directory: string) { - if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return; - - // Because we already watch node_modules, handle symlinks in there - if (!this.realpath || !stringContains(directory, nodeModulesPathPart)) return; - if (!this.symlinkedDirectories) this.symlinkedDirectories = createMap(); - const directoryPath = ensureTrailingDirectorySeparator(this.toPath(directory)); - if (this.symlinkedDirectories.has(directoryPath)) return; - - const real = normalizePath(this.projectService.host.realpath!(directory)); - let realPath: Path; - if (real === directory || - (realPath = ensureTrailingDirectorySeparator(this.toPath(real))) === directoryPath) { - // not symlinked - this.symlinkedDirectories.set(directoryPath, false); - return; - } - - this.symlinkedDirectories.set(directoryPath, { - real: ensureTrailingDirectorySeparator(real), - realPath - }); - } - - private fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean { - const fileOrDirectoryExistsUsingSource = isFile ? - (file: string) => this.fileExistsIfProjectReferenceDts(file) : - (dir: string) => this.directoryExistsIfProjectReferenceDeclDir(dir); - // Check current directory or file - const result = fileOrDirectoryExistsUsingSource(fileOrDirectory); - if (result !== undefined) return result; - - if (!this.symlinkedDirectories) return false; - const fileOrDirectoryPath = this.toPath(fileOrDirectory); - if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false; - if (isFile && this.symlinkedFiles && this.symlinkedFiles.has(fileOrDirectoryPath)) return true; - - // If it contains node_modules check if its one of the symlinked path we know of - return firstDefinedIterator( - this.symlinkedDirectories.entries(), - ([directoryPath, symlinkedDirectory]) => { - if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined; - const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath)); - if (isFile && result) { - if (!this.symlinkedFiles) this.symlinkedFiles = createMap(); - // Store the real path for the file' - const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, this.currentDirectory); - this.symlinkedFiles.set( - fileOrDirectoryPath, - `${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}` - ); - } - return result; - } - ) || false; + /* @internal */ + useSourceOfProjectReferenceRedirect() { + return this.languageServiceEnabled; } /* @internal */ @@ -2009,10 +1856,6 @@ namespace ts.server { this.isInitialLoadPending = returnFalse; const reloadLevel = this.pendingReload; this.pendingReload = ConfigFileProgramReloadLevel.None; - this.projectReferenceCallbacks = undefined; - this.mapOfDeclarationDirectories = undefined; - this.symlinkedDirectories = undefined; - this.symlinkedFiles = undefined; let result: boolean; switch (reloadLevel) { case ConfigFileProgramReloadLevel.Partial: @@ -2029,6 +1872,7 @@ namespace ts.server { default: result = super.updateGraph(); } + this.compilerHost = undefined; this.projectService.sendProjectLoadingFinishEvent(this); this.projectService.sendProjectTelemetry(this); return result; @@ -2146,11 +1990,8 @@ namespace ts.server { this.stopWatchingWildCards(); this.projectErrors = undefined; this.configFileSpecs = undefined; - this.projectReferenceCallbacks = undefined; - this.mapOfDeclarationDirectories = undefined; - this.symlinkedDirectories = undefined; - this.symlinkedFiles = undefined; this.openFileWatchTriggered.clear(); + this.compilerHost = undefined; super.close(); } diff --git a/src/services/services.ts b/src/services/services.ts index 753642ccdefbc..11056dd276d96 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1274,12 +1274,10 @@ namespace ts { if (host.resolveTypeReferenceDirectives) { compilerHost.resolveTypeReferenceDirectives = (...args) => host.resolveTypeReferenceDirectives!(...args); } - if (host.setResolvedProjectReferenceCallbacks) { - compilerHost.setResolvedProjectReferenceCallbacks = callbacks => host.setResolvedProjectReferenceCallbacks!(callbacks); - } if (host.useSourceOfProjectReferenceRedirect) { compilerHost.useSourceOfProjectReferenceRedirect = () => host.useSourceOfProjectReferenceRedirect!(); } + host.setCompilerHost?.(compilerHost); const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); const options: CreateProgramOptions = { diff --git a/src/services/types.ts b/src/services/types.ts index 1df82f8a7e9d9..5337863bf672a 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -274,7 +274,7 @@ namespace ts { /* @internal */ getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache; /* @internal */ - setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void; + setCompilerHost?(host: CompilerHost): void; /* @internal */ useSourceOfProjectReferenceRedirect?(): boolean; } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 5fdc05ae1ce03..bdd59ebf45d6d 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -138,8 +138,9 @@ "unittests/tscWatch/incremental.ts", "unittests/tscWatch/programUpdates.ts", "unittests/tscWatch/resolutionCache.ts", - "unittests/tscWatch/watchEnvironment.ts", + "unittests/tscWatch/sourceOfProjectReferenceRedirect.ts", "unittests/tscWatch/watchApi.ts", + "unittests/tscWatch/watchEnvironment.ts", "unittests/tsserver/applyChangesToOpenFiles.ts", "unittests/tsserver/cachingFileSystemInformation.ts", "unittests/tsserver/cancellationToken.ts", diff --git a/src/testRunner/unittests/tscWatch/helpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts index 5492b5cbfe63b..4e3d4dd126f27 100644 --- a/src/testRunner/unittests/tscWatch/helpers.ts +++ b/src/testRunner/unittests/tscWatch/helpers.ts @@ -282,7 +282,7 @@ namespace ts.tscWatch { scenario: string; subScenario: string; commandLineArgs: readonly string[]; - changes: TscWatchCompileChange[]; + changes: readonly TscWatchCompileChange[]; } export interface TscWatchCompile extends TscWatchCompileBase { sys: () => WatchedSystem; diff --git a/src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts b/src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts new file mode 100644 index 0000000000000..d293602f4a791 --- /dev/null +++ b/src/testRunner/unittests/tscWatch/sourceOfProjectReferenceRedirect.ts @@ -0,0 +1,158 @@ +namespace ts.tscWatch { + import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile; + describe("unittests:: tsc-watch:: watchAPI:: with sourceOfProjectReferenceRedirect", () => { + interface VerifyWatchInput { + files: readonly TestFSWithWatch.FileOrFolderOrSymLink[]; + config: string; + expectedProgramFiles: readonly string[]; + } + function verifyWatch( + { files, config, expectedProgramFiles }: VerifyWatchInput, + alreadyBuilt: boolean + ) { + const sys = createWatchedSystem(files); + if (alreadyBuilt) { + const solutionBuilder = createSolutionBuilder(sys, [config], {}); + solutionBuilder.build(); + solutionBuilder.close(); + sys.clearOutput(); + } + const host = createWatchCompilerHostOfConfigFile(config, {}, /*watchOptionsToExtend*/ undefined, sys); + host.useSourceOfProjectReferenceRedirect = returnTrue; + const watch = createWatchProgram(host); + checkProgramActualFiles(watch.getCurrentProgram().getProgram(), expectedProgramFiles); + } + + function verifyScenario(input: () => VerifyWatchInput) { + it("when solution is not built", () => { + verifyWatch(input(), /*alreadyBuilt*/ false); + }); + + it("when solution is already built", () => { + verifyWatch(input(), /*alreadyBuilt*/ true); + }); + } + + describe("with simple project", () => { + verifyScenario(() => { + const baseConfig = getFileFromProject("demo", "tsconfig-base.json"); + const coreTs = getFileFromProject("demo", "core/utilities.ts"); + const coreConfig = getFileFromProject("demo", "core/tsconfig.json"); + const animalTs = getFileFromProject("demo", "animals/animal.ts"); + const dogTs = getFileFromProject("demo", "animals/dog.ts"); + const indexTs = getFileFromProject("demo", "animals/index.ts"); + const animalsConfig = getFileFromProject("demo", "animals/tsconfig.json"); + return { + files: [{ path: libFile.path, content: libContent }, baseConfig, coreTs, coreConfig, animalTs, dogTs, indexTs, animalsConfig], + config: animalsConfig.path, + expectedProgramFiles: [libFile.path, indexTs.path, dogTs.path, animalTs.path, coreTs.path] + }; + }); + }); + + describe("when references are monorepo like with symlinks", () => { + interface Packages { + bPackageJson: File; + aTest: File; + bFoo: File; + bBar: File; + bSymlink: SymLink; + } + function verifySymlinkScenario(packages: () => Packages) { + describe("when preserveSymlinks is turned off", () => { + verifySymlinkScenarioWorker(packages, {}); + }); + describe("when preserveSymlinks is turned on", () => { + verifySymlinkScenarioWorker(packages, { preserveSymlinks: true }); + }); + } + + function verifySymlinkScenarioWorker(packages: () => Packages, extraOptions: CompilerOptions) { + verifyScenario(() => { + const { bPackageJson, aTest, bFoo, bBar, bSymlink } = packages(); + const aConfig = config("A", extraOptions, ["../B"]); + const bConfig = config("B", extraOptions); + return { + files: [libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink], + config: aConfig.path, + expectedProgramFiles: [libFile.path, aTest.path, bFoo.path, bBar.path] + }; + }); + } + + function config(packageName: string, extraOptions: CompilerOptions, references?: string[]): File { + return { + path: `${projectRoot}/packages/${packageName}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + outDir: "lib", + rootDir: "src", + composite: true, + ...extraOptions + }, + include: ["src"], + ...(references ? { references: references.map(path => ({ path })) } : {}) + }) + }; + } + + function file(packageName: string, fileName: string, content: string): File { + return { + path: `${projectRoot}/packages/${packageName}/src/${fileName}`, + content + }; + } + + function verifyMonoRepoLike(scope = "") { + describe("when packageJson has types field", () => { + verifySymlinkScenario(() => ({ + bPackageJson: { + path: `${projectRoot}/packages/B/package.json`, + content: JSON.stringify({ + main: "lib/index.js", + types: "lib/index.d.ts" + }) + }, + aTest: file("A", "index.ts", `import { foo } from '${scope}b'; +import { bar } from '${scope}b/lib/bar'; +foo(); +bar(); +`), + bFoo: file("B", "index.ts", `export function foo() { }`), + bBar: file("B", "bar.ts", `export function bar() { }`), + bSymlink: { + path: `${projectRoot}/node_modules/${scope}b`, + symLink: `${projectRoot}/packages/B` + } + })); + }); + + describe("when referencing file from subFolder", () => { + verifySymlinkScenario(() => ({ + bPackageJson: { + path: `${projectRoot}/packages/B/package.json`, + content: "{}" + }, + aTest: file("A", "test.ts", `import { foo } from '${scope}b/lib/foo'; +import { bar } from '${scope}b/lib/bar/foo'; +foo(); +bar(); +`), + bFoo: file("B", "foo.ts", `export function foo() { }`), + bBar: file("B", "bar/foo.ts", `export function bar() { }`), + bSymlink: { + path: `${projectRoot}/node_modules/${scope}b`, + symLink: `${projectRoot}/packages/B` + } + })); + }); + } + describe("when package is not scoped", () => { + verifyMonoRepoLike(); + }); + describe("when package is scoped", () => { + verifyMonoRepoLike("@issue/"); + }); + }); + }); +} diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6488461fc28e3..9cc07dc275628 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4758,6 +4758,8 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[]; } interface WatchCompilerHost extends ProgramHost, WatchHost { + /** Instead of using output d.ts file from project reference, use its source file */ + useSourceOfProjectReferenceRedirect?(): boolean; /** If provided, callback to invoke after every new program creation */ afterProgramCreate?(program: T): void; } @@ -8922,37 +8924,10 @@ declare namespace ts.server { private typeAcquisition; private directoriesWatchedForWildcards; readonly canonicalConfigFilePath: NormalizedPath; - private projectReferenceCallbacks; - private mapOfDeclarationDirectories; - private symlinkedDirectories; - private symlinkedFiles; /** Ref count to the project when opened from external project */ private externalProjectRefCount; private projectErrors; private projectReferences; - private fileExistsIfProjectReferenceDts; - /** - * This implementation of fileExists checks if the file being requested is - * .d.ts file for the referenced Project. - * If it is it returns true irrespective of whether that file exists on host - */ - fileExists(file: string): boolean; - private directoryExistsIfProjectReferenceDeclDir; - /** - * This implementation of directoryExists checks if the directory being requested is - * directory of .d.ts file for the referenced Project. - * If it is it returns true irrespective of whether that directory exists on host - */ - directoryExists(path: string): boolean; - /** - * Call super.getDirectories only if directory actually present on the host - * This is needed to ensure that we arent getting directories that we fake about presence for - */ - getDirectories(path: string): string[]; - private realpathIfSymlinkedProjectReferenceDts; - private getRealpath; - private handleDirectoryCouldBeSymlink; - private fileOrDirectoryExistsUsingSource; /** * 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. diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c3f629b355c8f..98b8eb2e2b152 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4758,6 +4758,8 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[]; } interface WatchCompilerHost extends ProgramHost, WatchHost { + /** Instead of using output d.ts file from project reference, use its source file */ + useSourceOfProjectReferenceRedirect?(): boolean; /** If provided, callback to invoke after every new program creation */ afterProgramCreate?(program: T): void; }