Skip to content

Commit 0383b5c

Browse files
authored
Optimize import fixes for projects with many symlinks (#42150)
* Create symlink cache when a pnpm module is found * Keep pnpm-internal symlinks out of the symlink cache * Filter out pnpm path from realpath module specifier too * Optimize symlink module specifier generation * Add trailing directory separators * Remove unneeded change * Fix paths losing case in cache * Fix missing absolutification
1 parent 9d286a2 commit 0383b5c

File tree

5 files changed

+59
-23
lines changed

5 files changed

+59
-23
lines changed

src/compiler/moduleSpecifiers.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ namespace ts.moduleSpecifiers {
286286
const getCanonicalFileName = hostGetCanonicalFileName(host);
287287
const cwd = host.getCurrentDirectory();
288288
const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined;
289-
const redirects = host.redirectTargetsMap.get(toPath(importedFileName, cwd, getCanonicalFileName)) || emptyArray;
289+
const importedPath = toPath(importedFileName, cwd, getCanonicalFileName);
290+
const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray;
290291
const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects];
291292
const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
292293
if (!preferSymlinks) {
@@ -299,22 +300,25 @@ namespace ts.moduleSpecifiers {
299300
? host.getSymlinkCache()
300301
: discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd);
301302

302-
const symlinkedDirectories = links.getSymlinkedDirectories();
303-
const useCaseSensitiveFileNames = !host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames();
304-
const result = symlinkedDirectories && forEachEntry(symlinkedDirectories, (resolved, path) => {
305-
if (resolved === false) return undefined;
306-
if (startsWithDirectory(importingFileName, resolved.realPath, getCanonicalFileName)) {
307-
return undefined; // Don't want to a package to globally import from itself
303+
const symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath();
304+
const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd);
305+
const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => {
306+
const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName)));
307+
if (!symlinkDirectories) return undefined; // Continue to ancestor directory
308+
309+
// Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts)
310+
if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) {
311+
return false; // Stop search, each ancestor directory will also hit this condition
308312
}
309313

310314
return forEach(targets, target => {
311-
if (!containsPath(resolved.real, target, !useCaseSensitiveFileNames)) {
315+
if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) {
312316
return;
313317
}
314318

315-
const relative = getRelativePathFromDirectory(resolved.real, target, getCanonicalFileName);
316-
const option = resolvePath(path, relative);
317-
if (!host.fileExists || host.fileExists(option)) {
319+
const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName);
320+
for (const symlinkDirectory of symlinkDirectories) {
321+
const option = resolvePath(symlinkDirectory, relative);
318322
const result = cb(option, target === referenceRedirect);
319323
if (result) return result;
320324
}

src/compiler/path.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ namespace ts {
762762
* Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback.
763763
* Comparison is case-sensitive between the canonical paths.
764764
*
765-
* @deprecated Use `containsPath` if possible.
765+
* Use `containsPath` if file names are not already reduced and absolute.
766766
*/
767767
export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean {
768768
const canonicalFileName = getCanonicalFileName(fileName);

src/compiler/program.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3777,7 +3777,7 @@ namespace ts {
37773777
return;
37783778
}
37793779

3780-
symlinkCache.setSymlinkedDirectory(directoryPath, {
3780+
symlinkCache.setSymlinkedDirectory(directory, {
37813781
real: ensureTrailingDirectorySeparator(real),
37823782
realPath
37833783
});

src/compiler/utilities.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -6083,49 +6083,60 @@ namespace ts {
60836083
}
60846084

60856085
export interface SymlinkCache {
6086+
/** Gets a map from symlink to realpath. Keys have trailing directory separators. */
60866087
getSymlinkedDirectories(): ReadonlyESMap<Path, SymlinkedDirectory | false> | undefined;
6088+
/** Gets a map from realpath to symlinks. Keys have trailing directory separators. */
6089+
getSymlinkedDirectoriesByRealpath(): MultiMap<Path, string> | undefined;
6090+
/** Gets a map from symlink to realpath */
60876091
getSymlinkedFiles(): ReadonlyESMap<Path, string> | undefined;
6088-
setSymlinkedDirectory(path: Path, directory: SymlinkedDirectory | false): void;
6089-
setSymlinkedFile(path: Path, real: string): void;
6092+
setSymlinkedDirectory(symlink: string, real: SymlinkedDirectory | false): void;
6093+
setSymlinkedFile(symlinkPath: Path, real: string): void;
60906094
}
60916095

6092-
export function createSymlinkCache(): SymlinkCache {
6096+
export function createSymlinkCache(cwd: string, getCanonicalFileName: GetCanonicalFileName): SymlinkCache {
60936097
let symlinkedDirectories: ESMap<Path, SymlinkedDirectory | false> | undefined;
6098+
let symlinkedDirectoriesByRealpath: MultiMap<Path, string> | undefined;
60946099
let symlinkedFiles: ESMap<Path, string> | undefined;
60956100
return {
60966101
getSymlinkedFiles: () => symlinkedFiles,
60976102
getSymlinkedDirectories: () => symlinkedDirectories,
6103+
getSymlinkedDirectoriesByRealpath: () => symlinkedDirectoriesByRealpath,
60986104
setSymlinkedFile: (path, real) => (symlinkedFiles || (symlinkedFiles = new Map())).set(path, real),
6099-
setSymlinkedDirectory: (path, directory) => {
6105+
setSymlinkedDirectory: (symlink, real) => {
61006106
// Large, interconnected dependency graphs in pnpm will have a huge number of symlinks
61016107
// where both the realpath and the symlink path are inside node_modules/.pnpm. Since
61026108
// this path is never a candidate for a module specifier, we can ignore it entirely.
6103-
if (!containsIgnoredPath(path)) {
6104-
(symlinkedDirectories || (symlinkedDirectories = new Map())).set(path, directory);
6109+
let symlinkPath = toPath(symlink, cwd, getCanonicalFileName);
6110+
if (!containsIgnoredPath(symlinkPath)) {
6111+
symlinkPath = ensureTrailingDirectorySeparator(symlinkPath);
6112+
if (real !== false && !symlinkedDirectories?.has(symlinkPath)) {
6113+
(symlinkedDirectoriesByRealpath ||= createMultiMap()).add(ensureTrailingDirectorySeparator(real.realPath), symlink);
6114+
}
6115+
(symlinkedDirectories || (symlinkedDirectories = new Map())).set(symlinkPath, real);
61056116
}
61066117
}
61076118
};
61086119
}
61096120

61106121
export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): SymlinkCache {
6111-
const cache = createSymlinkCache();
6122+
const cache = createSymlinkCache(cwd, getCanonicalFileName);
61126123
const symlinks = flatten<readonly [string, string]>(mapDefined(files, sf =>
61136124
sf.resolvedModules && compact(arrayFrom(mapIterator(sf.resolvedModules.values(), res =>
61146125
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] as const : undefined)))));
61156126
for (const [resolvedPath, originalPath] of symlinks) {
61166127
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName) || emptyArray;
61176128
if (commonResolved && commonOriginal) {
61186129
cache.setSymlinkedDirectory(
6119-
toPath(commonOriginal, cwd, getCanonicalFileName),
6130+
commonOriginal,
61206131
{ real: commonResolved, realPath: toPath(commonResolved, cwd, getCanonicalFileName) });
61216132
}
61226133
}
61236134
return cache;
61246135
}
61256136

61266137
function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] | undefined {
6127-
const aParts = getPathComponents(toPath(a, cwd, getCanonicalFileName));
6128-
const bParts = getPathComponents(toPath(b, cwd, getCanonicalFileName));
6138+
const aParts = getPathComponents(getNormalizedAbsolutePath(a, cwd));
6139+
const bParts = getPathComponents(getNormalizedAbsolutePath(b, cwd));
61296140
let isDirectory = false;
61306141
while (!isNodeModulesOrScopedPackageDirectory(aParts[aParts.length - 2], getCanonicalFileName) &&
61316142
!isNodeModulesOrScopedPackageDirectory(bParts[bParts.length - 2], getCanonicalFileName) &&
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /tsconfig.json
4+
//// { "compilerOptions": { "module": "commonjs" } }
5+
6+
// @Filename: /node_modules/.pnpm/[email protected]/node_modules/MobX/Foo.d.ts
7+
//// export declare function autorun(): void;
8+
9+
// @Filename: /index.ts
10+
//// autorun/**/
11+
12+
// @Filename: /utils.ts
13+
//// import "MobX/Foo";
14+
15+
// @link: /node_modules/.pnpm/[email protected]/node_modules/MobX -> /node_modules/MobX
16+
// @link: /node_modules/.pnpm/[email protected]/node_modules/MobX -> /node_modules/.pnpm/[email protected]/node_modules/MobX
17+
18+
goTo.marker("");
19+
verify.importFixAtPosition([`import { autorun } from "MobX/Foo";
20+
21+
autorun`]);

0 commit comments

Comments
 (0)