Skip to content

Make AutoImportProviderProject work with symlinked monorepos #39679

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

Merged
merged 11 commits into from
Jul 22, 2020
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4234,7 +4234,7 @@ namespace ts {
getCommonSourceDirectory: !!(host as Program).getCommonSourceDirectory ? () => (host as Program).getCommonSourceDirectory() : () => "",
getSourceFiles: () => host.getSourceFiles(),
getCurrentDirectory: () => host.getCurrentDirectory(),
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks),
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
redirectTargetsMap: host.redirectTargetsMap,
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),
Expand Down
14 changes: 8 additions & 6 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,20 +185,22 @@ namespace ts.moduleSpecifiers {
const result = forEach(targets, cb);
if (result) return result;
}
const links = host.getProbableSymlinks
? host.getProbableSymlinks(host.getSourceFiles())
const links = host.getSymlinkCache
? host.getSymlinkCache()
: discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd);

const symlinkedDirectories = links.getSymlinkedDirectories();
const compareStrings = (!host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames()) ? compareStringsCaseSensitive : compareStringsCaseInsensitive;
const result = forEachEntry(links, (resolved, path) => {
if (startsWithDirectory(importingFileName, resolved, getCanonicalFileName)) {
const result = symlinkedDirectories && forEachEntry(symlinkedDirectories, (resolved, path) => {
if (resolved === false) return undefined;
if (startsWithDirectory(importingFileName, resolved.realPath, getCanonicalFileName)) {
return undefined; // Don't want to a package to globally import from itself
}

const target = find(targets, t => compareStrings(t.slice(0, resolved.length + 1), resolved + "/") === Comparison.EqualTo);
const target = find(targets, t => compareStrings(t.slice(0, resolved.real.length), resolved.real) === Comparison.EqualTo);
if (target === undefined) return undefined;

const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName);
const relative = getRelativePathFromDirectory(resolved.real, target, getCanonicalFileName);
const option = resolvePath(path, relative);
if (!host.fileExists || host.fileExists(option)) {
const result = cb(option);
Expand Down
48 changes: 23 additions & 25 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ namespace ts {
let processingDefaultLibFiles: SourceFile[] | undefined;
let processingOtherFiles: SourceFile[] | undefined;
let files: SourceFile[];
let symlinks: ReadonlyESMap<string, string> | undefined;
let symlinks: SymlinkCache | undefined;
let commonSourceDirectory: string;
let diagnosticsProducingTypeChecker: TypeChecker;
let noDiagnosticsTypeChecker: TypeChecker;
Expand Down Expand Up @@ -811,8 +811,9 @@ namespace ts {

const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() &&
!options.disableSourceOfProjectReferenceRedirect;
const { onProgramCreateComplete, fileExists } = updateHostForUseSourceOfProjectReferenceRedirect({
const { onProgramCreateComplete, fileExists, directoryExists } = updateHostForUseSourceOfProjectReferenceRedirect({
compilerHost: host,
getSymlinkCache,
useSourceOfProjectReferenceRedirect,
toPath,
getResolvedProjectReferences,
Expand Down Expand Up @@ -974,7 +975,9 @@ namespace ts {
isSourceOfProjectReferenceRedirect,
emitBuildInfo,
fileExists,
getProbableSymlinks,
directoryExists,
getSymlinkCache,
realpath: host.realpath?.bind(host),
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
};

Expand Down Expand Up @@ -1490,7 +1493,7 @@ namespace ts {
getResolvedProjectReferenceToRedirect,
getProjectReferenceRedirect,
isSourceOfProjectReferenceRedirect,
getProbableSymlinks,
getSymlinkCache,
writeFile: writeFileCallback || (
(fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
isEmitBlocked,
Expand Down Expand Up @@ -3468,9 +3471,9 @@ namespace ts {
return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo;
}

function getProbableSymlinks(): ReadonlyESMap<string, string> {
if (host.getSymlinks) {
return host.getSymlinks();
function getSymlinkCache(): SymlinkCache {
if (host.getSymlinkCache) {
return host.getSymlinkCache();
}
return symlinks || (symlinks = discoverProbableSymlinks(
files,
Expand All @@ -3479,13 +3482,9 @@ namespace ts {
}
}

interface SymlinkedDirectory {
real: string;
realPath: Path;
}

interface HostForUseSourceOfProjectReferenceRedirect {
compilerHost: CompilerHost;
getSymlinkCache: () => SymlinkCache;
useSourceOfProjectReferenceRedirect: boolean;
toPath(fileName: string): Path;
getResolvedProjectReferences(): readonly (ResolvedProjectReference | undefined)[] | undefined;
Expand All @@ -3495,9 +3494,6 @@ namespace ts {

function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) {
let setOfDeclarationDirectories: Set<Path> | undefined;
let symlinkedDirectories: ESMap<Path, SymlinkedDirectory | false> | undefined;
let symlinkedFiles: ESMap<Path, string> | undefined;

const originalFileExists = host.compilerHost.fileExists;
const originalDirectoryExists = host.compilerHost.directoryExists;
const originalGetDirectories = host.compilerHost.getDirectories;
Expand All @@ -3507,11 +3503,12 @@ namespace ts {

host.compilerHost.fileExists = fileExists;

let directoryExists;
if (originalDirectoryExists) {
// 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
host.compilerHost.directoryExists = path => {
directoryExists = host.compilerHost.directoryExists = path => {
if (originalDirectoryExists.call(host.compilerHost, path)) {
handleDirectoryCouldBeSymlink(path);
return true;
Expand Down Expand Up @@ -3553,11 +3550,11 @@ namespace ts {
// This is something we keep for life time of the host
if (originalRealpath) {
host.compilerHost.realpath = s =>
symlinkedFiles?.get(host.toPath(s)) ||
host.getSymlinkCache().getSymlinkedFiles()?.get(host.toPath(s)) ||
originalRealpath.call(host.compilerHost, s);
}

return { onProgramCreateComplete, fileExists };
return { onProgramCreateComplete, fileExists, directoryExists };

function onProgramCreateComplete() {
host.compilerHost.fileExists = originalFileExists;
Expand Down Expand Up @@ -3603,20 +3600,20 @@ namespace ts {

// Because we already watch node_modules, handle symlinks in there
if (!originalRealpath || !stringContains(directory, nodeModulesPathPart)) return;
if (!symlinkedDirectories) symlinkedDirectories = new Map();
const symlinkCache = host.getSymlinkCache();
const directoryPath = ensureTrailingDirectorySeparator(host.toPath(directory));
if (symlinkedDirectories.has(directoryPath)) return;
if (symlinkCache.getSymlinkedDirectories()?.has(directoryPath)) return;

const real = normalizePath(originalRealpath.call(host.compilerHost, directory));
let realPath: Path;
if (real === directory ||
(realPath = ensureTrailingDirectorySeparator(host.toPath(real))) === directoryPath) {
// not symlinked
symlinkedDirectories.set(directoryPath, false);
symlinkCache.setSymlinkedDirectory(directoryPath, false);
return;
}

symlinkedDirectories.set(directoryPath, {
symlinkCache.setSymlinkedDirectory(directoryPath, {
real: ensureTrailingDirectorySeparator(real),
realPath
});
Expand All @@ -3630,10 +3627,12 @@ namespace ts {
const result = fileOrDirectoryExistsUsingSource(fileOrDirectory);
if (result !== undefined) return result;

const symlinkCache = host.getSymlinkCache();
const symlinkedDirectories = symlinkCache.getSymlinkedDirectories();
if (!symlinkedDirectories) return false;
const fileOrDirectoryPath = host.toPath(fileOrDirectory);
if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false;
if (isFile && symlinkedFiles && symlinkedFiles.has(fileOrDirectoryPath)) return true;
if (isFile && symlinkCache.getSymlinkedFiles()?.has(fileOrDirectoryPath)) return true;

// If it contains node_modules check if its one of the symlinked path we know of
return firstDefinedIterator(
Expand All @@ -3642,10 +3641,9 @@ namespace ts {
if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined;
const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath));
if (isFile && result) {
if (!symlinkedFiles) symlinkedFiles = new Map();
// Store the real path for the file'
const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, host.compilerHost.getCurrentDirectory());
symlinkedFiles.set(
symlinkCache.setSymlinkedFile(
fileOrDirectoryPath,
`${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`
);
Expand Down
7 changes: 4 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3767,7 +3767,6 @@ namespace ts {
/*@internal*/ isSourceOfProjectReferenceRedirect(fileName: string): boolean;
/*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined;
/*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
/*@internal*/ getProbableSymlinks(): ReadonlyESMap<string, string>;
/**
* This implementation handles file exists to be true if file is source of project reference redirect when program is created using useSourceOfProjectReferenceRedirect
*/
Expand Down Expand Up @@ -6240,7 +6239,7 @@ namespace ts {

// TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesn't use compilerHost as base
/*@internal*/createDirectory?(directory: string): void;
/*@internal*/getSymlinks?(): ReadonlyESMap<string, string>;
/*@internal*/getSymlinkCache?(): SymlinkCache;
}

/** true if --out otherwise source file name */
Expand Down Expand Up @@ -7754,8 +7753,10 @@ namespace ts {
useCaseSensitiveFileNames?(): boolean;
fileExists(path: string): boolean;
getCurrentDirectory(): string;
directoryExists?(path: string): boolean;
readFile?(path: string): string | undefined;
getProbableSymlinks?(files: readonly SourceFile[]): ReadonlyESMap<string, string>;
realpath?(path: string): string;
getSymlinkCache?(): SymlinkCache;
getGlobalTypingsCacheLocation?(): string | undefined;

getSourceFiles(): readonly SourceFile[];
Expand Down
43 changes: 36 additions & 7 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5926,28 +5926,57 @@ namespace ts {
return true;
}

export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): ReadonlyESMap<string, string> {
const result = new Map<string, string>();
export interface SymlinkedDirectory {
real: string;
realPath: Path;
}

export interface SymlinkCache {
getSymlinkedDirectories(): ReadonlyESMap<Path, SymlinkedDirectory | false> | undefined;
getSymlinkedFiles(): ReadonlyESMap<Path, string> | undefined;
setSymlinkedDirectory(path: Path, directory: SymlinkedDirectory | false): void;
setSymlinkedFile(path: Path, real: string): void;
}

export function createSymlinkCache(): SymlinkCache {
let symlinkedDirectories: ESMap<Path, SymlinkedDirectory | false> | undefined;
let symlinkedFiles: ESMap<Path, string> | undefined;
return {
getSymlinkedFiles: () => symlinkedFiles,
getSymlinkedDirectories: () => symlinkedDirectories,
setSymlinkedFile: (path, real) => (symlinkedFiles || (symlinkedFiles = new Map())).set(path, real),
setSymlinkedDirectory: (path, directory) => (symlinkedDirectories || (symlinkedDirectories = new Map())).set(path, directory),
};
}

export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): SymlinkCache {
const cache = createSymlinkCache();
const symlinks = flatten<readonly [string, string]>(mapDefined(files, sf =>
sf.resolvedModules && compact(arrayFrom(mapIterator(sf.resolvedModules.values(), res =>
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] as const : undefined)))));
for (const [resolvedPath, originalPath] of symlinks) {
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName);
result.set(commonOriginal, commonResolved);
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName) || emptyArray;
if (commonResolved && commonOriginal) {
cache.setSymlinkedDirectory(
toPath(commonOriginal, cwd, getCanonicalFileName),
{ real: commonResolved, realPath: toPath(commonResolved, cwd, getCanonicalFileName) });
}
}
return result;
return cache;
}

function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] {
function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] | undefined {
const aParts = getPathComponents(toPath(a, cwd, getCanonicalFileName));
const bParts = getPathComponents(toPath(b, cwd, getCanonicalFileName));
let isDirectory = false;
while (!isNodeModulesOrScopedPackageDirectory(aParts[aParts.length - 2], getCanonicalFileName) &&
!isNodeModulesOrScopedPackageDirectory(bParts[bParts.length - 2], getCanonicalFileName) &&
getCanonicalFileName(aParts[aParts.length - 1]) === getCanonicalFileName(bParts[bParts.length - 1])) {
aParts.pop();
bParts.pop();
isDirectory = true;
}
return [getPathFromPathComponents(aParts), getPathFromPathComponents(bParts)];
return isDirectory ? [getPathFromPathComponents(aParts), getPathFromPathComponents(bParts)] : undefined;
}

// KLUDGE: Don't assume one 'node_modules' links to another. More likely a single directory inside the node_modules is the symlink.
Expand Down
22 changes: 20 additions & 2 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ namespace Harness.LanguageService {
return fileNames;
}

public realpath(path: string): string {
try {
return this.vfs.realpathSync(path);
}
catch {
return path;
}
}

public directoryExists(path: string) {
return this.vfs.statSync(path).isDirectory();
}

public getScriptInfo(fileName: string): ScriptInfo | undefined {
return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName));
}
Expand Down Expand Up @@ -720,18 +733,23 @@ namespace Harness.LanguageService {
fileName = Compiler.defaultLibFileName;
}

const snapshot = this.host.getScriptSnapshot(fileName);
// System FS would follow symlinks, even though snapshots are stored by original file name
const snapshot = this.host.getScriptSnapshot(fileName) || this.host.getScriptSnapshot(this.realpath(fileName));
return snapshot && ts.getSnapshotText(snapshot);
}

realpath(path: string) {
return this.host.realpath(path);
}

writeFile = ts.noop;

resolvePath(path: string): string {
return path;
}

fileExists(path: string): boolean {
return !!this.host.getScriptSnapshot(path);
return this.host.fileExists(path);
}

directoryExists(): boolean {
Expand Down
Loading