diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 61fa4d1c69e2f..d83e72a12da2d 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -4233,6 +4233,7 @@ namespace ts {
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
redirectTargetsMap: host.redirectTargetsMap,
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),
+ getResolvedProjectReferenceToRedirect: fileName => host.getResolvedProjectReferenceToRedirect(fileName),
isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName),
fileExists: fileName => host.fileExists(fileName),
} : undefined },
diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts
index b173bb1336453..afba10da7ade3 100644
--- a/src/compiler/moduleSpecifiers.ts
+++ b/src/compiler/moduleSpecifiers.ts
@@ -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));
+ moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions, /*projectReferenceOutDir*/ undefined, /*packageNameOnly*/ true));
}
function getModuleSpecifierWorker(
@@ -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, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions, /*projectReferenceOutDir*/ undefined)) ||
getLocalModuleSpecifier(toFileName, info, compilerOptions, preferences);
}
@@ -98,11 +98,18 @@ namespace ts.moduleSpecifiers {
const info = getInfo(importingSourceFile.path, host);
const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol));
- const modulePaths = getAllModulePaths(importingSourceFile.path, moduleSourceFile.originalFileName, host);
+ const importedFileName = moduleSourceFile.originalFileName;
+ const modulePaths = getAllModulePaths(importingSourceFile.path, importedFileName, host);
+ const projectReference = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getResolvedProjectReferenceToRedirect(importedFileName) : undefined;
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 projectReferenceOutDir = projectReference?.commandLine.options.outDir;
+ const global = mapDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions, projectReferenceOutDir));
+ return global.length ? global : mapDefined(modulePaths, moduleFileName => {
+ if (!projectReferenceOutDir || !startsWith(moduleFileName, projectReferenceOutDir)) {
+ return getLocalModuleSpecifier(moduleFileName, info, compilerOptions, preferences);
+ }
+ });
}
interface Info {
@@ -195,15 +202,17 @@ 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.length + 1), resolved + "/") === Comparison.EqualTo);
- if (target === undefined) return undefined;
+ return forEach(targets, target => {
+ if (compareStrings(target.slice(0, resolved.length + 1), resolved + "/") !== Comparison.EqualTo) {
+ return;
+ }
- const relative = getRelativePathFromDirectory(resolved, 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, target, getCanonicalFileName);
+ const option = resolvePath(path, relative);
+ if (!host.fileExists || host.fileExists(option)) {
+ return cb(option);
+ }
+ });
});
return result ||
(preferSymlinks ? forEach(targets, cb) : undefined);
@@ -310,7 +319,7 @@ namespace ts.moduleSpecifiers {
: removeFileExtension(relativePath);
}
- function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {
+ function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, projectReferenceOutDir: string | undefined, packageNameOnly?: boolean): string | undefined {
if (!host.fileExists || !host.readFile) {
return undefined;
}
@@ -343,6 +352,15 @@ namespace ts.moduleSpecifiers {
}
}
+ // If this path is in the outDir of a referenced project, ignore it. It was important
+ // to see if it was the `types` or `main` of the package.json so that we could have
+ // returned just the package name as the module specifier. But since we cannot, it
+ // is preferable to return nothing so a different module specifier, outside of outDir,
+ // can be used instead.
+ if (projectReferenceOutDir && isInProjectReferenceOutDir(moduleSpecifier, parts, projectReferenceOutDir, getCanonicalFileName, host)) {
+ 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.
@@ -404,6 +422,19 @@ namespace ts.moduleSpecifiers {
}
}
+ function isInProjectReferenceOutDir(absoluteModuleSpecifier: string, moduleSpecifierParts: NodeModulePathParts, projectReferenceOutDir: string, getCanonicalFileName: GetCanonicalFileName, host: ModuleSpecifierResolutionHost) {
+ if (startsWith(absoluteModuleSpecifier, projectReferenceOutDir)) return true;
+ const packagePath = absoluteModuleSpecifier.slice(0, moduleSpecifierParts.packageRootIndex);
+ const allSourceFiles = host.getSourceFiles();
+ const links = host.getProbableSymlinks?.(allSourceFiles) || discoverProbableSymlinks(allSourceFiles, getCanonicalFileName, host.getCurrentDirectory());
+ const sourcePackagePath = links.get(packagePath);
+ if (sourcePackagePath) {
+ const sourcePath = combinePaths(sourcePackagePath, absoluteModuleSpecifier.slice(moduleSpecifierParts.packageRootIndex + 1));
+ return startsWith(sourcePath, projectReferenceOutDir);
+ }
+ return false;
+ }
+
function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) {
if (!host.fileExists) return;
// We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index bc53c6484626e..ce12826dbf3d0 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -7745,6 +7745,7 @@ namespace ts {
getSourceFiles(): readonly SourceFile[];
readonly redirectTargetsMap: RedirectTargetsMap;
getProjectReferenceRedirect(fileName: string): string | undefined;
+ getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined;
isSourceOfProjectReferenceRedirect(fileName: string): boolean;
}
diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts
index 1056d0329ee45..ae36335e070d8 100644
--- a/src/harness/fourslashImpl.ts
+++ b/src/harness/fourslashImpl.ts
@@ -2829,9 +2829,14 @@ namespace FourSlash {
const change = ts.first(codeFix.changes);
ts.Debug.assert(change.fileName === fileName);
this.applyEdits(change.fileName, change.textChanges);
- const text = range ? this.rangeText(range) : this.getFileContent(this.activeFile.fileName);
+ const text = range ? this.rangeText(range) : this.getFileContent(fileName);
actualTextArray.push(text);
- scriptInfo.updateContent(originalContent);
+
+ // Undo changes to perform next fix
+ const span = change.textChanges[0].span;
+ const deletedText = originalContent.substr(span.start, change.textChanges[0].span.length);
+ const insertedText = change.textChanges[0].newText;
+ this.editScriptAndUpdateMarkers(fileName, span.start, span.start + insertedText.length, deletedText);
}
if (expectedTextArray.length !== actualTextArray.length) {
this.raiseError(`Expected ${expectedTextArray.length} import fixes, got ${actualTextArray.length}`);
diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts
index b13b017559297..913901e2b10a1 100644
--- a/src/harness/harnessLanguageService.ts
+++ b/src/harness/harnessLanguageService.ts
@@ -154,14 +154,23 @@ namespace Harness.LanguageService {
return fileNames;
}
+ public realpath(path: string): string {
+ try {
+ return this.vfs.realpathSync(path);
+ }
+ catch {
+ return path;
+ }
+ }
+
public getScriptInfo(fileName: string): ScriptInfo | undefined {
- return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName));
+ return this.scriptInfos.get(this.realpath(vpath.resolve(this.vfs.cwd(), fileName)));
}
public addScript(fileName: string, content: string, isRootFile: boolean): void {
this.vfs.mkdirpSync(vpath.dirname(fileName));
this.vfs.writeFileSync(fileName, content);
- this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile));
+ this.scriptInfos.set(this.realpath(vpath.resolve(this.vfs.cwd(), fileName)), new ScriptInfo(fileName, content, isRootFile));
}
public renameFileOrDirectory(oldPath: string, newPath: string): void {
@@ -173,7 +182,7 @@ namespace Harness.LanguageService {
const newFileName = updater(key);
if (newFileName !== undefined) {
this.scriptInfos.delete(key);
- this.scriptInfos.set(newFileName, scriptInfo);
+ this.scriptInfos.set(this.realpath(newFileName), scriptInfo);
scriptInfo.fileName = newFileName;
}
});
@@ -703,6 +712,10 @@ namespace Harness.LanguageService {
this.writeMessage(message);
}
+ realpath(path: string) {
+ return this.host.realpath(path);
+ }
+
readFile(fileName: string): string | undefined {
if (ts.stringContains(fileName, Compiler.defaultLibFileName)) {
fileName = Compiler.defaultLibFileName;
diff --git a/src/services/utilities.ts b/src/services/utilities.ts
index d3b96994f61e9..c6beb4dd8d743 100644
--- a/src/services/utilities.ts
+++ b/src/services/utilities.ts
@@ -1736,6 +1736,7 @@ namespace ts {
getSourceFiles: () => program.getSourceFiles(),
redirectTargetsMap: program.redirectTargetsMap,
getProjectReferenceRedirect: fileName => program.getProjectReferenceRedirect(fileName),
+ getResolvedProjectReferenceToRedirect: fileName => program.getResolvedProjectReferenceToRedirect(fileName),
isSourceOfProjectReferenceRedirect: fileName => program.isSourceOfProjectReferenceRedirect(fileName),
};
}
diff --git a/tests/cases/fourslash/server/autoImportProjectReferenceRedirect1.ts b/tests/cases/fourslash/server/autoImportProjectReferenceRedirect1.ts
new file mode 100644
index 0000000000000..db42bf5206dd5
--- /dev/null
+++ b/tests/cases/fourslash/server/autoImportProjectReferenceRedirect1.ts
@@ -0,0 +1,29 @@
+///
+
+// @link: /packages/a -> /node_modules/a
+
+// @Filename: /packages/a/tsconfig.json
+//// { "compilerOptions": { "module": "commonjs", "composite": true, "outDir": "dist", "rootDir": "src" } }
+
+// @Filename: /packages/a/package.json
+//// { "name": "a", "main": "dist/index.js", "typings": "dist/index.d.ts" }
+
+// @Filename: /packages/a/src/index.ts
+//// import "./a";
+//// export class Index {}
+
+// @Filename: /packages/a/src/a.ts
+//// export class A {}
+
+
+// @Filename: /packages/b/tsconfig.json
+//// { "compilerOptions": { "module": "commonjs", "outDir": "dist", "rootDir": "src" }, "references": [{ "path": "../a" }] }
+
+// @Filename: /packages/b/src/util.ts
+//// import {} from "a";
+
+// @Filename: /packages/b/src/index.ts
+//// A/**/
+
+goTo.marker("");
+verify.importFixAtPosition([`import { A } from "a/src/a";\r\n\r\nA`]);
diff --git a/tests/cases/fourslash/server/autoImportProjectReferenceRedirect2.ts b/tests/cases/fourslash/server/autoImportProjectReferenceRedirect2.ts
new file mode 100644
index 0000000000000..60a4c5246c85a
--- /dev/null
+++ b/tests/cases/fourslash/server/autoImportProjectReferenceRedirect2.ts
@@ -0,0 +1,35 @@
+///
+
+// @Filename: /packages/a/tsconfig.json
+//// { "compilerOptions": { "module": "commonjs", "composite": true, "outDir": "dist", "rootDir": "src" } }
+
+// @Filename: /packages/a/src/index.ts
+//// import "./a";
+//// export class Index {}
+
+// @Filename: /packages/a/src/a.ts
+//// export class A {}
+
+// @Filename: /packages/b/tsconfig.json
+//// {
+//// "compilerOptions": {
+//// "module": "commonjs",
+//// "outDir": "dist",
+//// "rootDir": "src",
+//// "baseUrl": ".",
+//// "paths": {
+//// "a": ["../a/src/index"],
+//// "a/*": ["../a/*"]
+//// }
+//// },
+//// "references": [{ "path": "../a" }]
+//// }
+
+// @Filename: /packages/b/src/util.ts
+//// import {} from "a";
+
+// @Filename: /packages/b/src/index.ts
+//// A/**/
+
+goTo.marker("");
+verify.importFixAtPosition([`import { A } from "a/src/a";\r\n\r\nA`]);