Skip to content

Commit ec33814

Browse files
Make AutoImportProviderProject work with symlinked monorepos (#39679)
* Hack everything together * Add test * Remove realpath from program * Ensure symlinked directories are directories * Revert unnecessary change * Update baselines * Use host program realpath on AutoImportProviderProject files before program creation * Which fixes hasRoots() too * Apply suggestions from code review Co-authored-by: Sheetal Nandi <[email protected]> * Lint Co-authored-by: Sheetal Nandi <[email protected]>
1 parent 312a6f0 commit ec33814

File tree

12 files changed

+166
-54
lines changed

12 files changed

+166
-54
lines changed

src/compiler/checker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4237,7 +4237,7 @@ namespace ts {
42374237
getCommonSourceDirectory: !!(host as Program).getCommonSourceDirectory ? () => (host as Program).getCommonSourceDirectory() : () => "",
42384238
getSourceFiles: () => host.getSourceFiles(),
42394239
getCurrentDirectory: () => host.getCurrentDirectory(),
4240-
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks),
4240+
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
42414241
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
42424242
redirectTargetsMap: host.redirectTargetsMap,
42434243
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),

src/compiler/moduleSpecifiers.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,20 +185,22 @@ namespace ts.moduleSpecifiers {
185185
const result = forEach(targets, cb);
186186
if (result) return result;
187187
}
188-
const links = host.getProbableSymlinks
189-
? host.getProbableSymlinks(host.getSourceFiles())
188+
const links = host.getSymlinkCache
189+
? host.getSymlinkCache()
190190
: discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd);
191191

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

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

201-
const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName);
203+
const relative = getRelativePathFromDirectory(resolved.real, target, getCanonicalFileName);
202204
const option = resolvePath(path, relative);
203205
if (!host.fileExists || host.fileExists(option)) {
204206
const result = cb(option);

src/compiler/program.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ namespace ts {
702702
let processingDefaultLibFiles: SourceFile[] | undefined;
703703
let processingOtherFiles: SourceFile[] | undefined;
704704
let files: SourceFile[];
705-
let symlinks: ReadonlyESMap<string, string> | undefined;
705+
let symlinks: SymlinkCache | undefined;
706706
let commonSourceDirectory: string;
707707
let diagnosticsProducingTypeChecker: TypeChecker;
708708
let noDiagnosticsTypeChecker: TypeChecker;
@@ -811,8 +811,9 @@ namespace ts {
811811

812812
const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() &&
813813
!options.disableSourceOfProjectReferenceRedirect;
814-
const { onProgramCreateComplete, fileExists } = updateHostForUseSourceOfProjectReferenceRedirect({
814+
const { onProgramCreateComplete, fileExists, directoryExists } = updateHostForUseSourceOfProjectReferenceRedirect({
815815
compilerHost: host,
816+
getSymlinkCache,
816817
useSourceOfProjectReferenceRedirect,
817818
toPath,
818819
getResolvedProjectReferences,
@@ -974,7 +975,9 @@ namespace ts {
974975
isSourceOfProjectReferenceRedirect,
975976
emitBuildInfo,
976977
fileExists,
977-
getProbableSymlinks,
978+
directoryExists,
979+
getSymlinkCache,
980+
realpath: host.realpath?.bind(host),
978981
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
979982
};
980983

@@ -1490,7 +1493,7 @@ namespace ts {
14901493
getResolvedProjectReferenceToRedirect,
14911494
getProjectReferenceRedirect,
14921495
isSourceOfProjectReferenceRedirect,
1493-
getProbableSymlinks,
1496+
getSymlinkCache,
14941497
writeFile: writeFileCallback || (
14951498
(fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
14961499
isEmitBlocked,
@@ -3468,9 +3471,9 @@ namespace ts {
34683471
return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo;
34693472
}
34703473

3471-
function getProbableSymlinks(): ReadonlyESMap<string, string> {
3472-
if (host.getSymlinks) {
3473-
return host.getSymlinks();
3474+
function getSymlinkCache(): SymlinkCache {
3475+
if (host.getSymlinkCache) {
3476+
return host.getSymlinkCache();
34743477
}
34753478
return symlinks || (symlinks = discoverProbableSymlinks(
34763479
files,
@@ -3479,13 +3482,9 @@ namespace ts {
34793482
}
34803483
}
34813484

3482-
interface SymlinkedDirectory {
3483-
real: string;
3484-
realPath: Path;
3485-
}
3486-
34873485
interface HostForUseSourceOfProjectReferenceRedirect {
34883486
compilerHost: CompilerHost;
3487+
getSymlinkCache: () => SymlinkCache;
34893488
useSourceOfProjectReferenceRedirect: boolean;
34903489
toPath(fileName: string): Path;
34913490
getResolvedProjectReferences(): readonly (ResolvedProjectReference | undefined)[] | undefined;
@@ -3495,9 +3494,6 @@ namespace ts {
34953494

34963495
function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) {
34973496
let setOfDeclarationDirectories: Set<Path> | undefined;
3498-
let symlinkedDirectories: ESMap<Path, SymlinkedDirectory | false> | undefined;
3499-
let symlinkedFiles: ESMap<Path, string> | undefined;
3500-
35013497
const originalFileExists = host.compilerHost.fileExists;
35023498
const originalDirectoryExists = host.compilerHost.directoryExists;
35033499
const originalGetDirectories = host.compilerHost.getDirectories;
@@ -3507,11 +3503,12 @@ namespace ts {
35073503

35083504
host.compilerHost.fileExists = fileExists;
35093505

3506+
let directoryExists;
35103507
if (originalDirectoryExists) {
35113508
// This implementation of directoryExists checks if the directory being requested is
35123509
// directory of .d.ts file for the referenced Project.
35133510
// If it is it returns true irrespective of whether that directory exists on host
3514-
host.compilerHost.directoryExists = path => {
3511+
directoryExists = host.compilerHost.directoryExists = path => {
35153512
if (originalDirectoryExists.call(host.compilerHost, path)) {
35163513
handleDirectoryCouldBeSymlink(path);
35173514
return true;
@@ -3553,11 +3550,11 @@ namespace ts {
35533550
// This is something we keep for life time of the host
35543551
if (originalRealpath) {
35553552
host.compilerHost.realpath = s =>
3556-
symlinkedFiles?.get(host.toPath(s)) ||
3553+
host.getSymlinkCache().getSymlinkedFiles()?.get(host.toPath(s)) ||
35573554
originalRealpath.call(host.compilerHost, s);
35583555
}
35593556

3560-
return { onProgramCreateComplete, fileExists };
3557+
return { onProgramCreateComplete, fileExists, directoryExists };
35613558

35623559
function onProgramCreateComplete() {
35633560
host.compilerHost.fileExists = originalFileExists;
@@ -3603,20 +3600,20 @@ namespace ts {
36033600

36043601
// Because we already watch node_modules, handle symlinks in there
36053602
if (!originalRealpath || !stringContains(directory, nodeModulesPathPart)) return;
3606-
if (!symlinkedDirectories) symlinkedDirectories = new Map();
3603+
const symlinkCache = host.getSymlinkCache();
36073604
const directoryPath = ensureTrailingDirectorySeparator(host.toPath(directory));
3608-
if (symlinkedDirectories.has(directoryPath)) return;
3605+
if (symlinkCache.getSymlinkedDirectories()?.has(directoryPath)) return;
36093606

36103607
const real = normalizePath(originalRealpath.call(host.compilerHost, directory));
36113608
let realPath: Path;
36123609
if (real === directory ||
36133610
(realPath = ensureTrailingDirectorySeparator(host.toPath(real))) === directoryPath) {
36143611
// not symlinked
3615-
symlinkedDirectories.set(directoryPath, false);
3612+
symlinkCache.setSymlinkedDirectory(directoryPath, false);
36163613
return;
36173614
}
36183615

3619-
symlinkedDirectories.set(directoryPath, {
3616+
symlinkCache.setSymlinkedDirectory(directoryPath, {
36203617
real: ensureTrailingDirectorySeparator(real),
36213618
realPath
36223619
});
@@ -3630,10 +3627,12 @@ namespace ts {
36303627
const result = fileOrDirectoryExistsUsingSource(fileOrDirectory);
36313628
if (result !== undefined) return result;
36323629

3630+
const symlinkCache = host.getSymlinkCache();
3631+
const symlinkedDirectories = symlinkCache.getSymlinkedDirectories();
36333632
if (!symlinkedDirectories) return false;
36343633
const fileOrDirectoryPath = host.toPath(fileOrDirectory);
36353634
if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false;
3636-
if (isFile && symlinkedFiles && symlinkedFiles.has(fileOrDirectoryPath)) return true;
3635+
if (isFile && symlinkCache.getSymlinkedFiles()?.has(fileOrDirectoryPath)) return true;
36373636

36383637
// If it contains node_modules check if its one of the symlinked path we know of
36393638
return firstDefinedIterator(
@@ -3642,10 +3641,9 @@ namespace ts {
36423641
if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined;
36433642
const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath));
36443643
if (isFile && result) {
3645-
if (!symlinkedFiles) symlinkedFiles = new Map();
36463644
// Store the real path for the file'
36473645
const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, host.compilerHost.getCurrentDirectory());
3648-
symlinkedFiles.set(
3646+
symlinkCache.setSymlinkedFile(
36493647
fileOrDirectoryPath,
36503648
`${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`
36513649
);

src/compiler/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3767,7 +3767,6 @@ namespace ts {
37673767
/*@internal*/ isSourceOfProjectReferenceRedirect(fileName: string): boolean;
37683768
/*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined;
37693769
/*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
3770-
/*@internal*/ getProbableSymlinks(): ReadonlyESMap<string, string>;
37713770
/**
37723771
* This implementation handles file exists to be true if file is source of project reference redirect when program is created using useSourceOfProjectReferenceRedirect
37733772
*/
@@ -6243,7 +6242,7 @@ namespace ts {
62436242

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

62496248
/** true if --out otherwise source file name */
@@ -7757,8 +7756,10 @@ namespace ts {
77577756
useCaseSensitiveFileNames?(): boolean;
77587757
fileExists(path: string): boolean;
77597758
getCurrentDirectory(): string;
7759+
directoryExists?(path: string): boolean;
77607760
readFile?(path: string): string | undefined;
7761-
getProbableSymlinks?(files: readonly SourceFile[]): ReadonlyESMap<string, string>;
7761+
realpath?(path: string): string;
7762+
getSymlinkCache?(): SymlinkCache;
77627763
getGlobalTypingsCacheLocation?(): string | undefined;
77637764

77647765
getSourceFiles(): readonly SourceFile[];

src/compiler/utilities.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5926,28 +5926,57 @@ namespace ts {
59265926
return true;
59275927
}
59285928

5929-
export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): ReadonlyESMap<string, string> {
5930-
const result = new Map<string, string>();
5929+
export interface SymlinkedDirectory {
5930+
real: string;
5931+
realPath: Path;
5932+
}
5933+
5934+
export interface SymlinkCache {
5935+
getSymlinkedDirectories(): ReadonlyESMap<Path, SymlinkedDirectory | false> | undefined;
5936+
getSymlinkedFiles(): ReadonlyESMap<Path, string> | undefined;
5937+
setSymlinkedDirectory(path: Path, directory: SymlinkedDirectory | false): void;
5938+
setSymlinkedFile(path: Path, real: string): void;
5939+
}
5940+
5941+
export function createSymlinkCache(): SymlinkCache {
5942+
let symlinkedDirectories: ESMap<Path, SymlinkedDirectory | false> | undefined;
5943+
let symlinkedFiles: ESMap<Path, string> | undefined;
5944+
return {
5945+
getSymlinkedFiles: () => symlinkedFiles,
5946+
getSymlinkedDirectories: () => symlinkedDirectories,
5947+
setSymlinkedFile: (path, real) => (symlinkedFiles || (symlinkedFiles = new Map())).set(path, real),
5948+
setSymlinkedDirectory: (path, directory) => (symlinkedDirectories || (symlinkedDirectories = new Map())).set(path, directory),
5949+
};
5950+
}
5951+
5952+
export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): SymlinkCache {
5953+
const cache = createSymlinkCache();
59315954
const symlinks = flatten<readonly [string, string]>(mapDefined(files, sf =>
59325955
sf.resolvedModules && compact(arrayFrom(mapIterator(sf.resolvedModules.values(), res =>
59335956
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] as const : undefined)))));
59345957
for (const [resolvedPath, originalPath] of symlinks) {
5935-
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName);
5936-
result.set(commonOriginal, commonResolved);
5958+
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName) || emptyArray;
5959+
if (commonResolved && commonOriginal) {
5960+
cache.setSymlinkedDirectory(
5961+
toPath(commonOriginal, cwd, getCanonicalFileName),
5962+
{ real: commonResolved, realPath: toPath(commonResolved, cwd, getCanonicalFileName) });
5963+
}
59375964
}
5938-
return result;
5965+
return cache;
59395966
}
59405967

5941-
function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] {
5968+
function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] | undefined {
59425969
const aParts = getPathComponents(toPath(a, cwd, getCanonicalFileName));
59435970
const bParts = getPathComponents(toPath(b, cwd, getCanonicalFileName));
5971+
let isDirectory = false;
59445972
while (!isNodeModulesOrScopedPackageDirectory(aParts[aParts.length - 2], getCanonicalFileName) &&
59455973
!isNodeModulesOrScopedPackageDirectory(bParts[bParts.length - 2], getCanonicalFileName) &&
59465974
getCanonicalFileName(aParts[aParts.length - 1]) === getCanonicalFileName(bParts[bParts.length - 1])) {
59475975
aParts.pop();
59485976
bParts.pop();
5977+
isDirectory = true;
59495978
}
5950-
return [getPathFromPathComponents(aParts), getPathFromPathComponents(bParts)];
5979+
return isDirectory ? [getPathFromPathComponents(aParts), getPathFromPathComponents(bParts)] : undefined;
59515980
}
59525981

59535982
// KLUDGE: Don't assume one 'node_modules' links to another. More likely a single directory inside the node_modules is the symlink.

src/harness/harnessLanguageService.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,19 @@ namespace Harness.LanguageService {
154154
return fileNames;
155155
}
156156

157+
public realpath(path: string): string {
158+
try {
159+
return this.vfs.realpathSync(path);
160+
}
161+
catch {
162+
return path;
163+
}
164+
}
165+
166+
public directoryExists(path: string) {
167+
return this.vfs.statSync(path).isDirectory();
168+
}
169+
157170
public getScriptInfo(fileName: string): ScriptInfo | undefined {
158171
return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName));
159172
}
@@ -720,18 +733,23 @@ namespace Harness.LanguageService {
720733
fileName = Compiler.defaultLibFileName;
721734
}
722735

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

741+
realpath(path: string) {
742+
return this.host.realpath(path);
743+
}
744+
727745
writeFile = ts.noop;
728746

729747
resolvePath(path: string): string {
730748
return path;
731749
}
732750

733751
fileExists(path: string): boolean {
734-
return !!this.host.getScriptSnapshot(path);
752+
return this.host.fileExists(path);
735753
}
736754

737755
directoryExists(): boolean {

0 commit comments

Comments
 (0)