Skip to content

Commit d0f4e7e

Browse files
sheetalkamattypescript-bot
authored andcommitted
Cherry-pick PR microsoft#34743 into release-3.7
Component commits: 06fda26 Fix incorrect outDir usage instead of out 66f1a79 Handle symlinks of packages in mono repo like packages Fixes microsoft#34723
1 parent 0965fc5 commit d0f4e7e

File tree

4 files changed

+223
-18
lines changed

4 files changed

+223
-18
lines changed

src/compiler/program.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2277,7 +2277,16 @@ namespace ts {
22772277
// Get source file from normalized fileName
22782278
function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, refFile: RefFile | undefined, packageId: PackageId | undefined): SourceFile | undefined {
22792279
if (useSourceOfProjectReferenceRedirect) {
2280-
const source = getSourceOfProjectReferenceRedirect(fileName);
2280+
let source = getSourceOfProjectReferenceRedirect(fileName);
2281+
if (!source &&
2282+
host.realpath &&
2283+
options.preserveSymlinks &&
2284+
isDeclarationFileName(fileName) &&
2285+
stringContains(fileName, nodeModulesPathPart)) {
2286+
// use host's cached realpath
2287+
const realPath = host.realpath(fileName);
2288+
if (realPath !== fileName) source = getSourceOfProjectReferenceRedirect(realPath);
2289+
}
22812290
if (source) {
22822291
const file = isString(source) ?
22832292
findSourceFile(source, toPath(source), isDefaultLib, ignoreNoDefaultLib, refFile, packageId) :

src/server/project.ts

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ namespace ts.server {
258258
private compilerOptions: CompilerOptions,
259259
public compileOnSaveEnabled: boolean,
260260
directoryStructureHost: DirectoryStructureHost,
261-
currentDirectory: string | undefined) {
261+
currentDirectory: string | undefined,
262+
customRealpath?: (s: string) => string) {
262263
this.directoryStructureHost = directoryStructureHost;
263264
this.currentDirectory = this.projectService.getNormalizedAbsolutePath(currentDirectory || "");
264265
this.getCanonicalFileName = this.projectService.toCanonicalFileName;
@@ -286,7 +287,7 @@ namespace ts.server {
286287
}
287288

288289
if (host.realpath) {
289-
this.realpath = path => host.realpath!(path);
290+
this.realpath = customRealpath || (path => host.realpath!(path));
290291
}
291292

292293
// Use the current directory as resolution root only if the project created using current directory string
@@ -1657,6 +1658,12 @@ namespace ts.server {
16571658
}
16581659
}
16591660

1661+
/*@internal*/
1662+
interface SymlinkedDirectory {
1663+
real: string;
1664+
realPath: Path;
1665+
}
1666+
16601667
/**
16611668
* If a file is opened, the server will look for a tsconfig (or jsconfig)
16621669
* and if successfull create a ConfiguredProject for it.
@@ -1670,6 +1677,8 @@ namespace ts.server {
16701677
readonly canonicalConfigFilePath: NormalizedPath;
16711678
private projectReferenceCallbacks: ResolvedProjectReferenceCallbacks | undefined;
16721679
private mapOfDeclarationDirectories: Map<true> | undefined;
1680+
private symlinkedDirectories: Map<SymlinkedDirectory | false> | undefined;
1681+
private symlinkedFiles: Map<string> | undefined;
16731682

16741683
/* @internal */
16751684
pendingReload: ConfigFileProgramReloadLevel | undefined;
@@ -1711,7 +1720,9 @@ namespace ts.server {
17111720
/*compilerOptions*/ {},
17121721
/*compileOnSaveEnabled*/ false,
17131722
cachedDirectoryStructureHost,
1714-
getDirectoryPath(configFileName));
1723+
getDirectoryPath(configFileName),
1724+
projectService.host.realpath && (s => this.getRealpath(s))
1725+
);
17151726
this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName));
17161727
}
17171728

@@ -1724,18 +1735,34 @@ namespace ts.server {
17241735
useSourceOfProjectReferenceRedirect = () => !!this.languageServiceEnabled &&
17251736
!this.getCompilerOptions().disableSourceOfProjectReferenceRedirect;
17261737

1738+
private fileExistsIfProjectReferenceDts(file: string) {
1739+
const source = this.projectReferenceCallbacks!.getSourceOfProjectReferenceRedirect(file);
1740+
return source !== undefined ?
1741+
isString(source) ? super.fileExists(source) : true :
1742+
undefined;
1743+
}
1744+
17271745
/**
17281746
* This implementation of fileExists checks if the file being requested is
17291747
* .d.ts file for the referenced Project.
17301748
* If it is it returns true irrespective of whether that file exists on host
17311749
*/
17321750
fileExists(file: string): boolean {
1751+
if (super.fileExists(file)) return true;
1752+
if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false;
1753+
if (!isDeclarationFileName(file)) return false;
1754+
17331755
// Project references go to source file instead of .d.ts file
1734-
if (this.useSourceOfProjectReferenceRedirect() && this.projectReferenceCallbacks) {
1735-
const source = this.projectReferenceCallbacks.getSourceOfProjectReferenceRedirect(file);
1736-
if (source) return isString(source) ? super.fileExists(source) : true;
1737-
}
1738-
return super.fileExists(file);
1756+
return this.fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
1757+
}
1758+
1759+
private directoryExistsIfProjectReferenceDeclDir(dir: string) {
1760+
const dirPath = this.toPath(dir);
1761+
const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`;
1762+
return forEachKey(
1763+
this.mapOfDeclarationDirectories!,
1764+
declDirPath => dirPath === declDirPath || startsWith(declDirPath, dirPathWithTrailingDirectorySeparator)
1765+
);
17391766
}
17401767

17411768
/**
@@ -1744,14 +1771,17 @@ namespace ts.server {
17441771
* If it is it returns true irrespective of whether that directory exists on host
17451772
*/
17461773
directoryExists(path: string): boolean {
1747-
if (super.directoryExists(path)) return true;
1774+
if (super.directoryExists(path)) {
1775+
this.handleDirectoryCouldBeSymlink(path);
1776+
return true;
1777+
}
17481778
if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false;
17491779

17501780
if (!this.mapOfDeclarationDirectories) {
17511781
this.mapOfDeclarationDirectories = createMap();
17521782
this.projectReferenceCallbacks.forEachResolvedProjectReference(ref => {
17531783
if (!ref) return;
1754-
const out = ref.commandLine.options.outFile || ref.commandLine.options.outDir;
1784+
const out = ref.commandLine.options.outFile || ref.commandLine.options.out;
17551785
if (out) {
17561786
this.mapOfDeclarationDirectories!.set(getDirectoryPath(this.toPath(out)), true);
17571787
}
@@ -1764,12 +1794,74 @@ namespace ts.server {
17641794
}
17651795
});
17661796
}
1767-
const dirPath = this.toPath(path);
1768-
const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`;
1769-
return !!forEachKey(
1770-
this.mapOfDeclarationDirectories,
1771-
declDirPath => dirPath === declDirPath || startsWith(declDirPath, dirPathWithTrailingDirectorySeparator)
1772-
);
1797+
1798+
return this.fileOrDirectoryExistsUsingSource(path, /*isFile*/ false);
1799+
}
1800+
1801+
private realpathIfSymlinkedProjectReferenceDts(s: string): string | undefined {
1802+
return this.symlinkedFiles && this.symlinkedFiles.get(this.toPath(s));
1803+
}
1804+
1805+
private getRealpath(s: string): string {
1806+
return this.realpathIfSymlinkedProjectReferenceDts(s) ||
1807+
this.projectService.host.realpath!(s);
1808+
}
1809+
1810+
private handleDirectoryCouldBeSymlink(directory: string) {
1811+
if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return;
1812+
1813+
// Because we already watch node_modules, handle symlinks in there
1814+
if (!this.realpath || !stringContains(directory, nodeModulesPathPart)) return;
1815+
if (!this.symlinkedDirectories) this.symlinkedDirectories = createMap();
1816+
const directoryPath = ensureTrailingDirectorySeparator(this.toPath(directory));
1817+
if (this.symlinkedDirectories.has(directoryPath)) return;
1818+
1819+
const real = this.projectService.host.realpath!(directory);
1820+
let realPath: Path;
1821+
if (real === directory ||
1822+
(realPath = ensureTrailingDirectorySeparator(this.toPath(real))) === directoryPath) {
1823+
// not symlinked
1824+
this.symlinkedDirectories.set(directoryPath, false);
1825+
return;
1826+
}
1827+
1828+
this.symlinkedDirectories.set(directoryPath, {
1829+
real: ensureTrailingDirectorySeparator(real),
1830+
realPath
1831+
});
1832+
}
1833+
1834+
private fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean {
1835+
const fileOrDirectoryExistsUsingSource = isFile ?
1836+
(file: string) => this.fileExistsIfProjectReferenceDts(file) :
1837+
(dir: string) => this.directoryExistsIfProjectReferenceDeclDir(dir);
1838+
// Check current directory or file
1839+
const result = fileOrDirectoryExistsUsingSource(fileOrDirectory);
1840+
if (result !== undefined) return result;
1841+
1842+
if (!this.symlinkedDirectories) return false;
1843+
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
1844+
if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false;
1845+
if (isFile && this.symlinkedFiles && this.symlinkedFiles.has(fileOrDirectoryPath)) return true;
1846+
1847+
// If it contains node_modules check if its one of the symlinked path we know of
1848+
return firstDefinedIterator(
1849+
this.symlinkedDirectories.entries(),
1850+
([directoryPath, symlinkedDirectory]) => {
1851+
if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined;
1852+
const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath));
1853+
if (isFile && result) {
1854+
if (!this.symlinkedFiles) this.symlinkedFiles = createMap();
1855+
// Store the real path for the file'
1856+
const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, this.currentDirectory);
1857+
this.symlinkedFiles.set(
1858+
fileOrDirectoryPath,
1859+
`${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`
1860+
);
1861+
}
1862+
return result;
1863+
}
1864+
) || false;
17731865
}
17741866

17751867
/**
@@ -1782,6 +1874,8 @@ namespace ts.server {
17821874
this.pendingReload = ConfigFileProgramReloadLevel.None;
17831875
this.projectReferenceCallbacks = undefined;
17841876
this.mapOfDeclarationDirectories = undefined;
1877+
this.symlinkedDirectories = undefined;
1878+
this.symlinkedFiles = undefined;
17851879
let result: boolean;
17861880
switch (reloadLevel) {
17871881
case ConfigFileProgramReloadLevel.Partial:
@@ -1914,6 +2008,8 @@ namespace ts.server {
19142008
this.configFileSpecs = undefined;
19152009
this.projectReferenceCallbacks = undefined;
19162010
this.mapOfDeclarationDirectories = undefined;
2011+
this.symlinkedDirectories = undefined;
2012+
this.symlinkedFiles = undefined;
19172013
super.close();
19182014
}
19192015

src/testRunner/unittests/tsserver/projectReferences.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace ts.projectSystem {
22
describe("unittests:: tsserver:: with project references and tsbuild", () => {
3-
function createHost(files: readonly File[], rootNames: readonly string[]) {
3+
function createHost(files: readonly TestFSWithWatch.FileOrFolderOrSymLink[], rootNames: readonly string[]) {
44
const host = createServerHost(files);
55

66
// ts build should succeed
@@ -1373,5 +1373,97 @@ function foo() {
13731373
assert.isTrue(projectA.dirty);
13741374
projectA.updateGraph();
13751375
});
1376+
1377+
describe("when references are monorepo like with symlinks", () => {
1378+
function verifySession(alreadyBuilt: boolean, extraOptions: CompilerOptions) {
1379+
const bPackageJson: File = {
1380+
path: `${projectRoot}/packages/B/package.json`,
1381+
content: JSON.stringify({
1382+
main: "lib/index.js",
1383+
types: "lib/index.d.ts"
1384+
})
1385+
};
1386+
const aConfig = config("A", extraOptions, ["../B"]);
1387+
const bConfig = config("B", extraOptions);
1388+
const aIndex = index("A", `import { foo } from 'b';
1389+
import { bar } from 'b/lib/bar';
1390+
foo();
1391+
bar();`);
1392+
const bIndex = index("B", `export function foo() { }`);
1393+
const bBar: File = {
1394+
path: `${projectRoot}/packages/B/src/bar.ts`,
1395+
content: `export function bar() { }`
1396+
};
1397+
const bSymlink: SymLink = {
1398+
path: `${projectRoot}/node_modules/b`,
1399+
symLink: `${projectRoot}/packages/B`
1400+
};
1401+
1402+
const files = [libFile, bPackageJson, aConfig, bConfig, aIndex, bIndex, bBar, bSymlink];
1403+
const host = alreadyBuilt ?
1404+
createHost(files, [aConfig.path]) :
1405+
createServerHost(files);
1406+
1407+
// Create symlink in node module
1408+
const session = createSession(host, { canUseEvents: true });
1409+
openFilesForSession([aIndex], session);
1410+
const service = session.getProjectService();
1411+
const project = service.configuredProjects.get(aConfig.path.toLowerCase())!;
1412+
assert.deepEqual(project.getAllProjectErrors(), []);
1413+
checkProjectActualFiles(
1414+
project,
1415+
[aConfig.path, aIndex.path, bIndex.path, bBar.path, libFile.path]
1416+
);
1417+
verifyGetErrRequest({
1418+
host,
1419+
session,
1420+
expected: [
1421+
{ file: aIndex, syntax: [], semantic: [], suggestion: [] }
1422+
]
1423+
});
1424+
}
1425+
1426+
function verifySymlinkScenario(alreadyBuilt: boolean) {
1427+
it("with preserveSymlinks turned off", () => {
1428+
verifySession(alreadyBuilt, {});
1429+
});
1430+
1431+
it("with preserveSymlinks turned on", () => {
1432+
verifySession(alreadyBuilt, { preserveSymlinks: true });
1433+
});
1434+
}
1435+
1436+
describe("when solution is not built", () => {
1437+
verifySymlinkScenario(/*alreadyBuilt*/ false);
1438+
});
1439+
1440+
describe("when solution is already built", () => {
1441+
verifySymlinkScenario(/*alreadyBuilt*/ true);
1442+
});
1443+
1444+
function config(packageName: string, extraOptions: CompilerOptions, references?: string[]): File {
1445+
return {
1446+
path: `${projectRoot}/packages/${packageName}/tsconfig.json`,
1447+
content: JSON.stringify({
1448+
compilerOptions: {
1449+
baseUrl: ".",
1450+
outDir: "lib",
1451+
rootDir: "src",
1452+
composite: true,
1453+
...extraOptions
1454+
},
1455+
include: ["src"],
1456+
...(references ? { references: references.map(path => ({ path })) } : {})
1457+
})
1458+
};
1459+
}
1460+
1461+
function index(packageName: string, content: string): File {
1462+
return {
1463+
path: `${projectRoot}/packages/${packageName}/src/index.ts`,
1464+
content
1465+
};
1466+
}
1467+
});
13761468
});
13771469
}

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8684,23 +8684,31 @@ declare namespace ts.server {
86848684
readonly canonicalConfigFilePath: NormalizedPath;
86858685
private projectReferenceCallbacks;
86868686
private mapOfDeclarationDirectories;
8687+
private symlinkedDirectories;
8688+
private symlinkedFiles;
86878689
/** Ref count to the project when opened from external project */
86888690
private externalProjectRefCount;
86898691
private projectErrors;
86908692
private projectReferences;
86918693
protected isInitialLoadPending: () => boolean;
8694+
private fileExistsIfProjectReferenceDts;
86928695
/**
86938696
* This implementation of fileExists checks if the file being requested is
86948697
* .d.ts file for the referenced Project.
86958698
* If it is it returns true irrespective of whether that file exists on host
86968699
*/
86978700
fileExists(file: string): boolean;
8701+
private directoryExistsIfProjectReferenceDeclDir;
86988702
/**
86998703
* This implementation of directoryExists checks if the directory being requested is
87008704
* directory of .d.ts file for the referenced Project.
87018705
* If it is it returns true irrespective of whether that directory exists on host
87028706
*/
87038707
directoryExists(path: string): boolean;
8708+
private realpathIfSymlinkedProjectReferenceDts;
8709+
private getRealpath;
8710+
private handleDirectoryCouldBeSymlink;
8711+
private fileOrDirectoryExistsUsingSource;
87048712
/**
87058713
* If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph
87068714
* @returns: true if set of files in the project stays the same and false - otherwise.

0 commit comments

Comments
 (0)