From 06fda263f8e730cdedd76ba7edecc23cb92bb93c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 25 Oct 2019 09:28:51 -0700 Subject: [PATCH 1/3] Fix incorrect outDir usage instead of out --- src/server/project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/project.ts b/src/server/project.ts index 28f9166064d34..0933f9c34bfa4 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1754,7 +1754,7 @@ namespace ts.server { this.mapOfDeclarationDirectories = createMap(); this.projectReferenceCallbacks.forEachResolvedProjectReference(ref => { if (!ref) return; - const out = ref.commandLine.options.outFile || ref.commandLine.options.outDir; + const out = ref.commandLine.options.outFile || ref.commandLine.options.out; if (out) { this.mapOfDeclarationDirectories!.set(getDirectoryPath(this.toPath(out)), true); } From 66f1a79c44d8fd2633b12303a6b39302a428b69e Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 25 Oct 2019 09:18:10 -0700 Subject: [PATCH 2/3] Handle symlinks of packages in mono repo like packages Fixes #34723 --- src/compiler/program.ts | 11 +- src/server/project.ts | 126 +++++++++++++++--- .../unittests/tsserver/projectReferences.ts | 94 ++++++++++++- .../reference/api/tsserverlibrary.d.ts | 8 ++ 4 files changed, 222 insertions(+), 17 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 59b57bfa99a41..aa326e2f00963 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -2271,7 +2271,16 @@ namespace ts { // Get source file from normalized fileName function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, refFile: RefFile | undefined, packageId: PackageId | undefined): SourceFile | undefined { if (useSourceOfProjectReferenceRedirect) { - const source = getSourceOfProjectReferenceRedirect(fileName); + let source = getSourceOfProjectReferenceRedirect(fileName); + if (!source && + host.realpath && + options.preserveSymlinks && + isDeclarationFileName(fileName) && + stringContains(fileName, nodeModulesPathPart)) { + // use host's cached realpath + const realPath = host.realpath(fileName); + if (realPath !== fileName) source = getSourceOfProjectReferenceRedirect(realPath); + } if (source) { const file = isString(source) ? findSourceFile(source, toPath(source), isDefaultLib, ignoreNoDefaultLib, refFile, packageId) : diff --git a/src/server/project.ts b/src/server/project.ts index 0933f9c34bfa4..5121556f53b6d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -258,7 +258,8 @@ namespace ts.server { private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, directoryStructureHost: DirectoryStructureHost, - currentDirectory: string | undefined) { + currentDirectory: string | undefined, + customRealpath?: (s: string) => string) { this.directoryStructureHost = directoryStructureHost; this.currentDirectory = this.projectService.getNormalizedAbsolutePath(currentDirectory || ""); this.getCanonicalFileName = this.projectService.toCanonicalFileName; @@ -286,7 +287,7 @@ namespace ts.server { } if (host.realpath) { - this.realpath = path => host.realpath!(path); + this.realpath = customRealpath || (path => host.realpath!(path)); } // Use the current directory as resolution root only if the project created using current directory string @@ -1660,6 +1661,12 @@ 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 successfull create a ConfiguredProject for it. @@ -1673,6 +1680,8 @@ namespace ts.server { readonly canonicalConfigFilePath: NormalizedPath; private projectReferenceCallbacks: ResolvedProjectReferenceCallbacks | undefined; private mapOfDeclarationDirectories: Map | undefined; + private symlinkedDirectories: Map | undefined; + private symlinkedFiles: Map | undefined; /* @internal */ pendingReload: ConfigFileProgramReloadLevel | undefined; @@ -1714,7 +1723,9 @@ namespace ts.server { /*compilerOptions*/ {}, /*compileOnSaveEnabled*/ false, cachedDirectoryStructureHost, - getDirectoryPath(configFileName)); + getDirectoryPath(configFileName), + projectService.host.realpath && (s => this.getRealpath(s)) + ); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); } @@ -1727,18 +1738,34 @@ namespace ts.server { 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 - if (this.useSourceOfProjectReferenceRedirect() && this.projectReferenceCallbacks) { - const source = this.projectReferenceCallbacks.getSourceOfProjectReferenceRedirect(file); - if (source) return isString(source) ? super.fileExists(source) : true; - } - return super.fileExists(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 || startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) + ); } /** @@ -1747,7 +1774,10 @@ namespace ts.server { * If it is it returns true irrespective of whether that directory exists on host */ directoryExists(path: string): boolean { - if (super.directoryExists(path)) return true; + if (super.directoryExists(path)) { + this.handleDirectoryCouldBeSymlink(path); + return true; + } if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false; if (!this.mapOfDeclarationDirectories) { @@ -1767,12 +1797,74 @@ namespace ts.server { } }); } - const dirPath = this.toPath(path); - const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`; - return !!forEachKey( - this.mapOfDeclarationDirectories, - declDirPath => dirPath === declDirPath || startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) - ); + + return this.fileOrDirectoryExistsUsingSource(path, /*isFile*/ false); + } + + 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); + } + + 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 = 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; } /** @@ -1785,6 +1877,8 @@ namespace ts.server { this.pendingReload = ConfigFileProgramReloadLevel.None; this.projectReferenceCallbacks = undefined; this.mapOfDeclarationDirectories = undefined; + this.symlinkedDirectories = undefined; + this.symlinkedFiles = undefined; let result: boolean; switch (reloadLevel) { case ConfigFileProgramReloadLevel.Partial: @@ -1917,6 +2011,8 @@ namespace ts.server { this.configFileSpecs = undefined; this.projectReferenceCallbacks = undefined; this.mapOfDeclarationDirectories = undefined; + this.symlinkedDirectories = undefined; + this.symlinkedFiles = undefined; super.close(); } diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index bbc368b6d8a33..0966ad911e356 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -1,6 +1,6 @@ namespace ts.projectSystem { describe("unittests:: tsserver:: with project references and tsbuild", () => { - function createHost(files: readonly File[], rootNames: readonly string[]) { + function createHost(files: readonly TestFSWithWatch.FileOrFolderOrSymLink[], rootNames: readonly string[]) { const host = createServerHost(files); // ts build should succeed @@ -1373,5 +1373,97 @@ function foo() { assert.isTrue(projectA.dirty); projectA.updateGraph(); }); + + describe("when references are monorepo like with symlinks", () => { + function verifySession(alreadyBuilt: boolean, extraOptions: CompilerOptions) { + const bPackageJson: File = { + path: `${projectRoot}/packages/B/package.json`, + content: JSON.stringify({ + main: "lib/index.js", + types: "lib/index.d.ts" + }) + }; + const aConfig = config("A", extraOptions, ["../B"]); + const bConfig = config("B", extraOptions); + const aIndex = index("A", `import { foo } from 'b'; +import { bar } from 'b/lib/bar'; +foo(); +bar();`); + const bIndex = index("B", `export function foo() { }`); + const bBar: File = { + path: `${projectRoot}/packages/B/src/bar.ts`, + content: `export function bar() { }` + }; + const bSymlink: SymLink = { + path: `${projectRoot}/node_modules/b`, + symLink: `${projectRoot}/packages/B` + }; + + const files = [libFile, bPackageJson, aConfig, bConfig, aIndex, bIndex, bBar, bSymlink]; + const host = alreadyBuilt ? + createHost(files, [aConfig.path]) : + createServerHost(files); + + // Create symlink in node module + const session = createSession(host, { canUseEvents: true }); + openFilesForSession([aIndex], session); + const service = session.getProjectService(); + const project = service.configuredProjects.get(aConfig.path.toLowerCase())!; + assert.deepEqual(project.getAllProjectErrors(), []); + checkProjectActualFiles( + project, + [aConfig.path, aIndex.path, bIndex.path, bBar.path, libFile.path] + ); + verifyGetErrRequest({ + host, + session, + expected: [ + { file: aIndex, syntax: [], semantic: [], suggestion: [] } + ] + }); + } + + function verifySymlinkScenario(alreadyBuilt: boolean) { + it("with preserveSymlinks turned off", () => { + verifySession(alreadyBuilt, {}); + }); + + it("with preserveSymlinks turned on", () => { + verifySession(alreadyBuilt, { preserveSymlinks: true }); + }); + } + + describe("when solution is not built", () => { + verifySymlinkScenario(/*alreadyBuilt*/ false); + }); + + describe("when solution is already built", () => { + verifySymlinkScenario(/*alreadyBuilt*/ true); + }); + + function config(packageName: string, extraOptions: CompilerOptions, references?: string[]): File { + return { + path: `${projectRoot}/packages/${packageName}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: ".", + outDir: "lib", + rootDir: "src", + composite: true, + ...extraOptions + }, + include: ["src"], + ...(references ? { references: references.map(path => ({ path })) } : {}) + }) + }; + } + + function index(packageName: string, content: string): File { + return { + path: `${projectRoot}/packages/${packageName}/src/index.ts`, + content + }; + } + }); }); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index c43f28c6e9095..754107de35bca 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8683,23 +8683,31 @@ declare namespace ts.server { 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; protected isInitialLoadPending: () => boolean; + 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; + 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. From 840f0c78b3473bbd6ade1626b21969056fed44d6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 29 Oct 2019 10:47:51 -0700 Subject: [PATCH 3/3] Added clarified comment --- src/compiler/program.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index aa326e2f00963..c9592d55cf961 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -2272,12 +2272,15 @@ namespace ts { function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, refFile: RefFile | undefined, packageId: PackageId | undefined): SourceFile | undefined { if (useSourceOfProjectReferenceRedirect) { let source = getSourceOfProjectReferenceRedirect(fileName); + // If preserveSymlinks is true, module resolution wont jump the symlink + // but the resolved real path may be the .d.ts from project reference + // Note:: Currently we try the real path only if the + // file is from node_modules to avoid having to run real path on all file paths if (!source && host.realpath && options.preserveSymlinks && isDeclarationFileName(fileName) && stringContains(fileName, nodeModulesPathPart)) { - // use host's cached realpath const realPath = host.realpath(fileName); if (realPath !== fileName) source = getSourceOfProjectReferenceRedirect(realPath); }