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
This commit is contained in:
Andrew Branch 2021-01-20 12:15:36 -08:00 committed by GitHub
parent 9d286a21ae
commit 0383b5cb4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 23 deletions

View File

@ -286,7 +286,8 @@ namespace ts.moduleSpecifiers {
const getCanonicalFileName = hostGetCanonicalFileName(host);
const cwd = host.getCurrentDirectory();
const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined;
const redirects = host.redirectTargetsMap.get(toPath(importedFileName, cwd, getCanonicalFileName)) || emptyArray;
const importedPath = toPath(importedFileName, cwd, getCanonicalFileName);
const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray;
const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects];
const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
if (!preferSymlinks) {
@ -299,22 +300,25 @@ namespace ts.moduleSpecifiers {
? host.getSymlinkCache()
: discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd);
const symlinkedDirectories = links.getSymlinkedDirectories();
const useCaseSensitiveFileNames = !host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames();
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 symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath();
const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd);
const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => {
const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName)));
if (!symlinkDirectories) return undefined; // Continue to ancestor directory
// Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts)
if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) {
return false; // Stop search, each ancestor directory will also hit this condition
}
return forEach(targets, target => {
if (!containsPath(resolved.real, target, !useCaseSensitiveFileNames)) {
if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) {
return;
}
const relative = getRelativePathFromDirectory(resolved.real, target, getCanonicalFileName);
const option = resolvePath(path, relative);
if (!host.fileExists || host.fileExists(option)) {
const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName);
for (const symlinkDirectory of symlinkDirectories) {
const option = resolvePath(symlinkDirectory, relative);
const result = cb(option, target === referenceRedirect);
if (result) return result;
}

View File

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

View File

@ -3777,7 +3777,7 @@ namespace ts {
return;
}
symlinkCache.setSymlinkedDirectory(directoryPath, {
symlinkCache.setSymlinkedDirectory(directory, {
real: ensureTrailingDirectorySeparator(real),
realPath
});

View File

@ -6083,32 +6083,43 @@ namespace ts {
}
export interface SymlinkCache {
/** Gets a map from symlink to realpath. Keys have trailing directory separators. */
getSymlinkedDirectories(): ReadonlyESMap<Path, SymlinkedDirectory | false> | undefined;
/** Gets a map from realpath to symlinks. Keys have trailing directory separators. */
getSymlinkedDirectoriesByRealpath(): MultiMap<Path, string> | undefined;
/** Gets a map from symlink to realpath */
getSymlinkedFiles(): ReadonlyESMap<Path, string> | undefined;
setSymlinkedDirectory(path: Path, directory: SymlinkedDirectory | false): void;
setSymlinkedFile(path: Path, real: string): void;
setSymlinkedDirectory(symlink: string, real: SymlinkedDirectory | false): void;
setSymlinkedFile(symlinkPath: Path, real: string): void;
}
export function createSymlinkCache(): SymlinkCache {
export function createSymlinkCache(cwd: string, getCanonicalFileName: GetCanonicalFileName): SymlinkCache {
let symlinkedDirectories: ESMap<Path, SymlinkedDirectory | false> | undefined;
let symlinkedDirectoriesByRealpath: MultiMap<Path, string> | undefined;
let symlinkedFiles: ESMap<Path, string> | undefined;
return {
getSymlinkedFiles: () => symlinkedFiles,
getSymlinkedDirectories: () => symlinkedDirectories,
getSymlinkedDirectoriesByRealpath: () => symlinkedDirectoriesByRealpath,
setSymlinkedFile: (path, real) => (symlinkedFiles || (symlinkedFiles = new Map())).set(path, real),
setSymlinkedDirectory: (path, directory) => {
setSymlinkedDirectory: (symlink, real) => {
// Large, interconnected dependency graphs in pnpm will have a huge number of symlinks
// where both the realpath and the symlink path are inside node_modules/.pnpm. Since
// this path is never a candidate for a module specifier, we can ignore it entirely.
if (!containsIgnoredPath(path)) {
(symlinkedDirectories || (symlinkedDirectories = new Map())).set(path, directory);
let symlinkPath = toPath(symlink, cwd, getCanonicalFileName);
if (!containsIgnoredPath(symlinkPath)) {
symlinkPath = ensureTrailingDirectorySeparator(symlinkPath);
if (real !== false && !symlinkedDirectories?.has(symlinkPath)) {
(symlinkedDirectoriesByRealpath ||= createMultiMap()).add(ensureTrailingDirectorySeparator(real.realPath), symlink);
}
(symlinkedDirectories || (symlinkedDirectories = new Map())).set(symlinkPath, real);
}
}
};
}
export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): SymlinkCache {
const cache = createSymlinkCache();
const cache = createSymlinkCache(cwd, getCanonicalFileName);
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)))));
@ -6116,7 +6127,7 @@ namespace ts {
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName) || emptyArray;
if (commonResolved && commonOriginal) {
cache.setSymlinkedDirectory(
toPath(commonOriginal, cwd, getCanonicalFileName),
commonOriginal,
{ real: commonResolved, realPath: toPath(commonResolved, cwd, getCanonicalFileName) });
}
}
@ -6124,8 +6135,8 @@ namespace ts {
}
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));
const aParts = getPathComponents(getNormalizedAbsolutePath(a, cwd));
const bParts = getPathComponents(getNormalizedAbsolutePath(b, cwd));
let isDirectory = false;
while (!isNodeModulesOrScopedPackageDirectory(aParts[aParts.length - 2], getCanonicalFileName) &&
!isNodeModulesOrScopedPackageDirectory(bParts[bParts.length - 2], getCanonicalFileName) &&

View File

@ -0,0 +1,21 @@
/// <reference path="fourslash.ts" />
// @Filename: /tsconfig.json
//// { "compilerOptions": { "module": "commonjs" } }
// @Filename: /node_modules/.pnpm/mobx@6.0.4/node_modules/MobX/Foo.d.ts
//// export declare function autorun(): void;
// @Filename: /index.ts
//// autorun/**/
// @Filename: /utils.ts
//// import "MobX/Foo";
// @link: /node_modules/.pnpm/mobx@6.0.4/node_modules/MobX -> /node_modules/MobX
// @link: /node_modules/.pnpm/mobx@6.0.4/node_modules/MobX -> /node_modules/.pnpm/cool-mobx-dependent@1.2.3/node_modules/MobX
goTo.marker("");
verify.importFixAtPosition([`import { autorun } from "MobX/Foo";
autorun`]);