diff --git a/src/server/project.ts b/src/server/project.ts index b0d2c427aa7d9..1a114a2d16b39 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1762,7 +1762,11 @@ namespace ts.server { const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`; return forEachKey( this.mapOfDeclarationDirectories!, - declDirPath => dirPath === declDirPath || startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) + declDirPath => dirPath === declDirPath || + // Any parent directory of declaration dir + startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) || + // Any directory inside declaration dir + startsWith(dirPath, `${declDirPath}/`) ); } @@ -1799,6 +1803,16 @@ namespace ts.server { 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)); } @@ -1817,7 +1831,7 @@ namespace ts.server { const directoryPath = ensureTrailingDirectorySeparator(this.toPath(directory)); if (this.symlinkedDirectories.has(directoryPath)) return; - const real = this.projectService.host.realpath!(directory); + const real = normalizePath(this.projectService.host.realpath!(directory)); let realPath: Path; if (real === directory || (realPath = ensureTrailingDirectorySeparator(this.toPath(real))) === directoryPath) { diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index 0966ad911e356..cde95f6dd52fe 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -1375,78 +1375,70 @@ function foo() { }); 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" - }) - }; + interface Packages { + bPackageJson: File; + aTest: File; + bFoo: File; + bBar: File; + } + function verifySymlinkScenario(packages: () => Packages) { + describe("when solution is not built", () => { + it("with preserveSymlinks turned off", () => { + verifySession(packages(), /*alreadyBuilt*/ false, {}); + }); + + it("with preserveSymlinks turned on", () => { + verifySession(packages(), /*alreadyBuilt*/ false, { preserveSymlinks: true }); + }); + }); + + describe("when solution is already built", () => { + it("with preserveSymlinks turned off", () => { + verifySession(packages(), /*alreadyBuilt*/ true, {}); + }); + + it("with preserveSymlinks turned on", () => { + verifySession(packages(), /*alreadyBuilt*/ true, { preserveSymlinks: true }); + }); + }); + } + + function verifySession({ bPackageJson, aTest, bFoo, bBar }: Packages, alreadyBuilt: boolean, extraOptions: CompilerOptions) { 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 files = [libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, 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); + openFilesForSession([aTest], 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] + [aConfig.path, aTest.path, bFoo.path, bBar.path, libFile.path] ); verifyGetErrRequest({ host, session, expected: [ - { file: aIndex, syntax: [], semantic: [], suggestion: [] } + { file: aTest, 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, @@ -1458,12 +1450,45 @@ bar();`); }; } - function index(packageName: string, content: string): File { + function file(packageName: string, fileName: string, content: string): File { return { - path: `${projectRoot}/packages/${packageName}/src/index.ts`, + path: `${projectRoot}/packages/${packageName}/src/${fileName}`, content }; } + + describe("when packageJson has types field and has index.ts", () => { + 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 'b'; +import { bar } from 'b/lib/bar'; +foo(); +bar();`), + bFoo: file("B", "index.ts", `export function foo() { }`), + bBar: file("B", "bar.ts", `export function bar() { }`) + })); + }); + + describe("when referencing file from subFolder", () => { + verifySymlinkScenario(() => ({ + bPackageJson: { + path: `${projectRoot}/packages/B/package.json`, + content: "{}" + }, + aTest: file("A", "test.ts", `import { foo } from 'b/lib/foo'; +import { bar } from 'b/lib/bar/foo'; +foo(); +bar();`), + bFoo: file("B", "foo.ts", `export function foo() { }`), + bBar: file("B", "bar/foo.ts", `export function bar() { }`) + })); + }); }); }); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6fbc32952f844..c6a532b6d7ad3 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8705,6 +8705,11 @@ declare namespace ts.server { * 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;