Reprioritize cross-project module specifier suggestions for auto-import by andrewbranch · Pull Request #40253 · microsoft/TypeScript (original) (raw)

Expand Up

@@ -69,7 +69,7 @@ namespace ts.moduleSpecifiers {

const info = getInfo(importingSourceFileName, host);

const modulePaths = getAllModulePaths(importingSourceFileName, nodeModulesFileName, host);

return firstDefined(modulePaths,

moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions, /*packageNameOnly*/ true));

modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions, /*packageNameOnly*/ true));

}

function getModuleSpecifierWorker(

Expand All

@@ -81,7 +81,7 @@ namespace ts.moduleSpecifiers {

): string {

const info = getInfo(importingSourceFileName, host);

const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host);

return firstDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions)) ||

return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions)) ||

getLocalModuleSpecifier(toFileName, info, compilerOptions, preferences);

}

Expand All

@@ -101,8 +101,48 @@ namespace ts.moduleSpecifiers {

const modulePaths = getAllModulePaths(importingSourceFile.path, moduleSourceFile.originalFileName, host);

const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile);

const global = mapDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions));

return global.length ? global : modulePaths.map(moduleFileName => getLocalModuleSpecifier(moduleFileName, info, compilerOptions, preferences));

const importedFileIsInNodeModules = some(modulePaths, p => p.isInNodeModules);

// Module specifier priority:

// 1. "Bare package specifiers" (e.g. "@foo/bar") resulting from a path through node_modules to a package.json's "types" entry

// 2. Specifiers generated using "paths" from tsconfig

// 3. Non-relative specfiers resulting from a path through node_modules (e.g. "@foo/bar/path/to/file")

// 4. Relative paths

let nodeModulesSpecifiers: string[] | undefined;

let pathsSpecifiers: string[] | undefined;

let relativeSpecifiers: string[] | undefined;

for (const modulePath of modulePaths) {

const specifier = tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions);

nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier);

if (specifier && modulePath.isRedirect) {

// If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",

// not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking.

return nodeModulesSpecifiers!;

}

if (!specifier && !modulePath.isRedirect) {

const local = getLocalModuleSpecifier(modulePath.path, info, compilerOptions, preferences);

if (pathIsBareSpecifier(local)) {

pathsSpecifiers = append(pathsSpecifiers, local);

}

else if (!importedFileIsInNodeModules || modulePath.isInNodeModules) {

// Why this extra conditional, not just an `else`? If some path to the file contained

// 'node_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"),

// that means we had to go through a *sibling's* node_modules, not one we can access directly.

// If some path to the file was in node_modules but another was not, this likely indicates that

// we have a monorepo structure with symlinks. In this case, the non-node_modules path is

// probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package

// in a monorepo is probably not portable. So, the module specifier we actually go with will be

// the relative path through node_modules, so that the declaration emitter can produce a

// portability error. (See declarationEmitReexportedSymlinkReference3)

relativeSpecifiers = append(relativeSpecifiers, local);

}

}

}

return pathsSpecifiers?.length ? pathsSpecifiers :

nodeModulesSpecifiers?.length ? nodeModulesSpecifiers :

Debug.checkDefined(relativeSpecifiers);

}

interface Info {

Expand Down Expand Up

@@ -161,10 +201,10 @@ namespace ts.moduleSpecifiers {

return match ? match.length : 0;

}

function comparePathsByNumberOfDirectorySeparators(a: string, b: string) {

return compareValues(

numberOfDirectorySeparators(a),

numberOfDirectorySeparators(b)

function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) {

return compareBooleans(b.isRedirect, a.isRedirect) || compareValues(

numberOfDirectorySeparators(a.path),

numberOfDirectorySeparators(b.path)

);

}

Expand All

@@ -173,7 +213,7 @@ namespace ts.moduleSpecifiers {

importedFileName: string,

host: ModuleSpecifierResolutionHost,

preferSymlinks: boolean,

cb: (fileName: string) => T | undefined

cb: (fileName: string, isRedirect: boolean) => T | undefined

): T | undefined {

const getCanonicalFileName = hostGetCanonicalFileName(host);

const cwd = host.getCurrentDirectory();

Expand All

@@ -182,7 +222,7 @@ namespace ts.moduleSpecifiers {

const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects];

const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));

if (!preferSymlinks) {

const result = forEach(targets, cb);

const result = forEach(targets, p => cb(p, referenceRedirect === p));

if (result) return result;

}

const links = host.getSymlinkCache

Expand All

@@ -197,61 +237,68 @@ namespace ts.moduleSpecifiers {

return undefined; // Don't want to a package to globally import from itself

}

const target = find(targets, t => compareStrings(t.slice(0, resolved.real.length), resolved.real) === Comparison.EqualTo);

if (target === undefined) return undefined;

return forEach(targets, target => {

if (compareStrings(target.slice(0, resolved.real.length), resolved.real) !== Comparison.EqualTo) {

return;

}

const relative = getRelativePathFromDirectory(resolved.real, target, getCanonicalFileName);

const option = resolvePath(path, relative);

if (!host.fileExists || host.fileExists(option)) {

const result = cb(option);

if (result) return result;

}

const relative = getRelativePathFromDirectory(resolved.real, target, getCanonicalFileName);

const option = resolvePath(path, relative);

if (!host.fileExists || host.fileExists(option)) {

const result = cb(option, target === referenceRedirect);

if (result) return result;

}

});

});

return result ||

(preferSymlinks ? forEach(targets, cb) : undefined);

(preferSymlinks ? forEach(targets, p => cb(p, p === referenceRedirect)) : undefined);

}

interface ModulePath {

path: string;

isInNodeModules: boolean;

isRedirect: boolean;

}

/**

* Looks for existing imports that use symlinks to this module.

* Symlinks will be returned first so they are preferred over the real path.

*/

function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly string[] {

function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly ModulePath[] {

const cwd = host.getCurrentDirectory();

const getCanonicalFileName = hostGetCanonicalFileName(host);

const allFileNames = new Map<string, string>();

const allFileNames = new Map<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>();

let importedFileFromNodeModules = false;

forEachFileNameOfModule(

importingFileName,

importedFileName,

host,

/*preferSymlinks*/ true,

path => {

(path, isRedirect) => {

const isInNodeModules = pathContainsNodeModules(path);

allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules });

importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;

// dont return value, so we collect everything

allFileNames.set(path, getCanonicalFileName(path));

importedFileFromNodeModules = importedFileFromNodeModules || pathContainsNodeModules(path);

}

);

// Sort by paths closest to importing file Name directory

const sortedPaths: string[] = [];

const sortedPaths: ModulePath[] = [];

for (

let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName));

allFileNames.size !== 0;

) {

const directoryStart = ensureTrailingDirectorySeparator(directory);

let pathsInDirectory: string[] | undefined;

allFileNames.forEach((canonicalFileName, fileName) => {

if (startsWith(canonicalFileName, directoryStart)) {

// If the importedFile is from node modules, use only paths in node_modules folder as option

if (!importedFileFromNodeModules || pathContainsNodeModules(fileName)) {

(pathsInDirectory || (pathsInDirectory = [])).push(fileName);

}

let pathsInDirectory: ModulePath[] | undefined;

allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => {

if (startsWith(path, directoryStart)) {

(pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules });

allFileNames.delete(fileName);

}

});

if (pathsInDirectory) {

if (pathsInDirectory.length > 1) {

pathsInDirectory.sort(comparePathsByNumberOfDirectorySeparators);

pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators);

}

sortedPaths.push(...pathsInDirectory);

}

Expand All

@@ -261,7 +308,7 @@ namespace ts.moduleSpecifiers {

}

if (allFileNames.size) {

const remainingPaths = arrayFrom(allFileNames.values());

if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByNumberOfDirectorySeparators);

if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators);

sortedPaths.push(...remainingPaths);

}

return sortedPaths;

Expand Down Expand Up

@@ -312,18 +359,19 @@ namespace ts.moduleSpecifiers {

: removeFileExtension(relativePath);

}

function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {

function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {

if (!host.fileExists || !host.readFile) {

return undefined;

}

const parts: NodeModulePathParts = getNodeModulePathParts(moduleFileName)!;

const parts: NodeModulePathParts = getNodeModulePathParts(path)!;

if (!parts) {

return undefined;

}

// Simplify the full file path to something that can be resolved by Node.

let moduleSpecifier = moduleFileName;

let moduleSpecifier = path;

let isPackageRootPath = false;

if (!packageNameOnly) {

let packageRootIndex = parts.packageRootIndex;

let moduleFileNameForExtensionless: string | undefined;

Expand All

@@ -332,19 +380,24 @@ namespace ts.moduleSpecifiers {

const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex);

if (packageRootPath) {

moduleSpecifier = packageRootPath;

isPackageRootPath = true;

break;

}

if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry;

// try with next level of directory

packageRootIndex = moduleFileName.indexOf(directorySeparator, packageRootIndex + 1);

packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1);

if (packageRootIndex === -1) {

moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless);

break;

}

}

}

if (isRedirect && !isPackageRootPath) {

return undefined;

}

const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();

// Get a path that's relative to node_modules or the importing file's path

// if node_modules folder is in this folder or any of its parent folders, no need to keep it.

Expand All

@@ -360,16 +413,16 @@ namespace ts.moduleSpecifiers {

return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName;

function tryDirectoryWithPackageJson(packageRootIndex: number) {

const packageRootPath = moduleFileName.substring(0, packageRootIndex);

const packageRootPath = path.substring(0, packageRootIndex);

const packageJsonPath = combinePaths(packageRootPath, "package.json");

let moduleFileToTry = moduleFileName;

let moduleFileToTry = path;

if (host.fileExists(packageJsonPath)) {

const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!);

const versionPaths = packageJsonContent.typesVersions

? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)

: undefined;

if (versionPaths) {

const subModuleName = moduleFileName.slice(packageRootPath.length + 1);

const subModuleName = path.slice(packageRootPath.length + 1);

const fromPaths = tryGetModuleNameFromPaths(

removeFileExtension(subModuleName),

removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options),

Expand Down