Skip to content

Commit b259d12

Browse files
committed
When the imported module is through node_modules and symlink to folder that isnt node_modules
Most of the monorepo like scenarios are like this so looking at symlink to decide if file can be imported is essential Fixes #28689
1 parent bb53936 commit b259d12

File tree

2 files changed

+80
-21
lines changed

2 files changed

+80
-21
lines changed

src/compiler/moduleSpecifiers.ts

+43-13
Original file line numberDiff line numberDiff line change
@@ -178,40 +178,70 @@ namespace ts.moduleSpecifiers {
178178
);
179179
}
180180

181-
/**
182-
* Looks for existing imports that use symlinks to this module.
183-
* Symlinks will be returned first so they are preferred over the real path.
184-
*/
185-
function getAllModulePaths(files: readonly SourceFile[], importingFileName: string, importedFileName: string, getCanonicalFileName: GetCanonicalFileName, host: ModuleSpecifierResolutionHost, redirectTargetsMap: RedirectTargetsMap): readonly string[] {
181+
export function forEachFileNameOfModule<T>(
182+
files: readonly SourceFile[],
183+
importingFileName: string,
184+
importedFileName: string,
185+
getCanonicalFileName: GetCanonicalFileName,
186+
host: ModuleSpecifierResolutionHost,
187+
redirectTargetsMap: RedirectTargetsMap,
188+
preferSymlinks: boolean,
189+
cb: (fileName: string) => T | undefined
190+
): T | undefined {
186191
const redirects = redirectTargetsMap.get(importedFileName);
187192
const importedFileNames = redirects ? [...redirects, importedFileName] : [importedFileName];
188193
const cwd = host.getCurrentDirectory ? host.getCurrentDirectory() : "";
189194
const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
195+
if (!preferSymlinks) {
196+
const result = forEach(targets, cb);
197+
if (result) return result;
198+
}
190199
const links = host.getProbableSymlinks
191200
? host.getProbableSymlinks(files)
192201
: discoverProbableSymlinks(files, getCanonicalFileName, cwd);
193202

194-
const result: string[] = [];
195203
const compareStrings = (!host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames()) ? compareStringsCaseSensitive : compareStringsCaseInsensitive;
196-
links.forEach((resolved, path) => {
204+
const result = forEachEntry(links, (resolved, path) => {
197205
if (startsWithDirectory(importingFileName, resolved, getCanonicalFileName)) {
198-
return; // Don't want to a package to globally import from itself
206+
return undefined; // Don't want to a package to globally import from itself
199207
}
200208

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

204212
const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName);
205213
const option = resolvePath(path, relative);
206214
if (!host.fileExists || host.fileExists(option)) {
207-
result.push(option);
215+
const result = cb(option);
216+
if (result) return result;
208217
}
209218
});
210-
result.push(...targets);
211-
if (result.length < 2) return result;
219+
return result ||
220+
(preferSymlinks ? forEach(targets, cb) : undefined);
221+
}
222+
223+
/**
224+
* Looks for existing imports that use symlinks to this module.
225+
* Symlinks will be returned first so they are preferred over the real path.
226+
*/
227+
function getAllModulePaths(files: readonly SourceFile[], importingFileName: string, importedFileName: string, getCanonicalFileName: GetCanonicalFileName, host: ModuleSpecifierResolutionHost, redirectTargetsMap: RedirectTargetsMap): readonly string[] {
228+
const cwd = host.getCurrentDirectory ? host.getCurrentDirectory() : "";
229+
const allFileNames = createMap<string>();
230+
forEachFileNameOfModule(
231+
files,
232+
importingFileName,
233+
importedFileName,
234+
getCanonicalFileName,
235+
host,
236+
redirectTargetsMap,
237+
/*preferSymlinks*/ true,
238+
path => {
239+
// dont return value, so we collect everything
240+
allFileNames.set(path, getCanonicalFileName(path));
241+
}
242+
);
212243

213244
// Sort by paths closest to importing file Name directory
214-
const allFileNames = arrayToMap(result, identity, getCanonicalFileName);
215245
const sortedPaths: string[] = [];
216246
for (
217247
let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName));

src/services/codefixes/importFixes.ts

+37-8
Original file line numberDiff line numberDiff line change
@@ -786,9 +786,9 @@ namespace ts.codefix {
786786
cb: (module: Symbol) => void,
787787
) {
788788
let filteredCount = 0;
789-
const packageJson = filterByPackageJson && createAutoImportFilter(from, program, host);
789+
const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host);
790+
const packageJson = filterByPackageJson && createAutoImportFilter(from, program, host, moduleSpecifierResolutionHost);
790791
const allSourceFiles = program.getSourceFiles();
791-
const globalTypingsCache = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
792792
forEachExternalModule(program.getTypeChecker(), allSourceFiles, (module, sourceFile) => {
793793
if (sourceFile === undefined) {
794794
if (!packageJson || packageJson.allowsImportingAmbientModule(module, allSourceFiles)) {
@@ -800,7 +800,7 @@ namespace ts.codefix {
800800
}
801801
else if (sourceFile &&
802802
sourceFile !== from &&
803-
isImportablePath(from.fileName, sourceFile.fileName, hostGetCanonicalFileName(host), globalTypingsCache)
803+
isImportableFile(program, from, sourceFile, moduleSpecifierResolutionHost)
804804
) {
805805
if (!packageJson || packageJson.allowsImportingSourceFile(sourceFile, allSourceFiles)) {
806806
cb(module);
@@ -826,6 +826,32 @@ namespace ts.codefix {
826826
}
827827
}
828828

829+
function isImportableFile(
830+
program: Program,
831+
from: SourceFile,
832+
to: SourceFile,
833+
moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost
834+
) {
835+
const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost);
836+
const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.();
837+
return !!moduleSpecifiers.forEachFileNameOfModule(
838+
program.getSourceFiles(),
839+
from.fileName,
840+
to.fileName,
841+
hostGetCanonicalFileName(moduleSpecifierResolutionHost),
842+
moduleSpecifierResolutionHost,
843+
program.redirectTargetsMap,
844+
/*preferSymlinks*/ false,
845+
toPath => {
846+
const toFile = program.getSourceFile(toPath);
847+
// Determine to import using toPath only if toPath is what we were looking at
848+
// or there doesnt exist the file in the program by the symlink
849+
return (toFile === to || !toFile) &&
850+
isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache);
851+
}
852+
);
853+
}
854+
829855
/**
830856
* Don't include something from a `node_modules` that isn't actually reachable by a global import.
831857
* A relative import to node_modules is usually a bad idea.
@@ -870,12 +896,10 @@ namespace ts.codefix {
870896
return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`;
871897
}
872898

873-
function createAutoImportFilter(fromFile: SourceFile, program: Program, host: LanguageServiceHost) {
874-
const packageJsons = host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName) || getPackageJsonsVisibleToFile(fromFile.fileName, host);
875-
const dependencyGroups = PackageJsonDependencyGroup.Dependencies | PackageJsonDependencyGroup.DevDependencies | PackageJsonDependencyGroup.OptionalDependencies;
899+
function createModuleSpecifierResolutionHost(program: Program, host: LanguageServiceHost): ModuleSpecifierResolutionHost {
876900
// Mix in `getProbablySymlinks` from Program when host doesn't have it
877901
// in order for non-Project hosts to have a symlinks cache.
878-
const moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost = {
902+
return {
879903
directoryExists: maybeBind(host, host.directoryExists),
880904
fileExists: maybeBind(host, host.fileExists),
881905
getCurrentDirectory: maybeBind(host, host.getCurrentDirectory),
@@ -884,9 +908,14 @@ namespace ts.codefix {
884908
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks) || program.getProbableSymlinks,
885909
getGlobalTypingsCacheLocation: maybeBind(host, host.getGlobalTypingsCacheLocation),
886910
};
911+
}
912+
913+
function createAutoImportFilter(fromFile: SourceFile, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host)) {
914+
const packageJsons = host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName) || getPackageJsonsVisibleToFile(fromFile.fileName, host);
915+
const dependencyGroups = PackageJsonDependencyGroup.Dependencies | PackageJsonDependencyGroup.DevDependencies | PackageJsonDependencyGroup.OptionalDependencies;
887916

888917
let usesNodeCoreModules: boolean | undefined;
889-
return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier };
918+
return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier, moduleSpecifierResolutionHost };
890919

891920
function moduleSpecifierIsCoveredByPackageJson(specifier: string) {
892921
const packageName = getNodeModuleRootSpecifier(specifier);

0 commit comments

Comments
 (0)