🤖 Cherry-pick PR #34743 into release-3.7 (#34781)

Component commits:
06fda263f8 Fix incorrect outDir usage instead of out

66f1a79c44 Handle symlinks of packages in mono repo like packages Fixes #34723
This commit is contained in:
TypeScript Bot 2019-10-28 16:44:03 -07:00 committed by Daniel Rosenwasser
parent 17bd75d613
commit 1f3016fdb3
4 changed files with 223 additions and 18 deletions

View File

@ -2277,7 +2277,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) :

View File

@ -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
@ -1657,6 +1658,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.
@ -1670,6 +1677,8 @@ namespace ts.server {
readonly canonicalConfigFilePath: NormalizedPath;
private projectReferenceCallbacks: ResolvedProjectReferenceCallbacks | undefined;
private mapOfDeclarationDirectories: Map<true> | undefined;
private symlinkedDirectories: Map<SymlinkedDirectory | false> | undefined;
private symlinkedFiles: Map<string> | undefined;
/* @internal */
pendingReload: ConfigFileProgramReloadLevel | undefined;
@ -1711,7 +1720,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));
}
@ -1724,18 +1735,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)
);
}
/**
@ -1744,14 +1771,17 @@ 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) {
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);
}
@ -1764,12 +1794,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;
}
/**
@ -1782,6 +1874,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:
@ -1914,6 +2008,8 @@ namespace ts.server {
this.configFileSpecs = undefined;
this.projectReferenceCallbacks = undefined;
this.mapOfDeclarationDirectories = undefined;
this.symlinkedDirectories = undefined;
this.symlinkedFiles = undefined;
super.close();
}

View File

@ -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
};
}
});
});
}

View File

@ -8684,23 +8684,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.