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`]);