Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🤖 Cherry-pick PR #35366 into release-3.7 #35368

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}/`)
);
}

Expand Down Expand Up @@ -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));
}
Expand All @@ -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) {
Expand Down
111 changes: 68 additions & 43 deletions src/testRunner/unittests/tsserver/projectReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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() { }`)
}));
});
});
});
}
5 changes: 5 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down