diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts
index d08bc6487e6..2d48f98141f 100644
--- a/src/services/exportInfoMap.ts
+++ b/src/services/exportInfoMap.ts
@@ -422,7 +422,16 @@ export function forEachExternalModuleToImportFrom(
const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.();
if (autoImportProvider) {
const start = timestamp();
- forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), excludePatterns, (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true));
+ const checker = program.getTypeChecker();
+ forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), excludePatterns, (module, file) => {
+ if (file && !program.getSourceFile(file.fileName) || !file && !checker.resolveName(module.name, /*location*/ undefined, SymbolFlags.Module, /*excludeGlobals*/ false)) {
+ // The AutoImportProvider filters files already in the main program out of its *root* files,
+ // but non-root files can still be present in both programs, and already in the export info map
+ // at this point. This doesn't create any incorrect behavior, but is a waste of time and memory,
+ // so we filter them out here.
+ cb(module, file, autoImportProvider, /*isFromPackageJson*/ true);
+ }
+ });
host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`);
}
}
diff --git a/src/testRunner/unittests/tsserver/autoImportProvider.ts b/src/testRunner/unittests/tsserver/autoImportProvider.ts
index 5fdd48039fe..ee27115d8a8 100644
--- a/src/testRunner/unittests/tsserver/autoImportProvider.ts
+++ b/src/testRunner/unittests/tsserver/autoImportProvider.ts
@@ -260,6 +260,41 @@ describe("unittests:: tsserver:: autoImportProvider", () => {
const project = projectService.configuredProjects.get(tsconfig.path)!;
assert.isUndefined(project.getPackageJsonAutoImportProvider());
});
+
+ it("Shared source files between AutoImportProvider and main program do not cause duplicate entries in export info map", () => {
+ const files = [
+ // node_modules/memfs - AutoImportProvider only
+ { path: "/node_modules/memfs/package.json", content: `{ "name": "memfs", "version": "1.0.0", "types": "lib/index.d.ts" }` },
+ { path: "/node_modules/memfs/lib/index.d.ts", content: `/// \nexport declare class Volume {}` },
+
+ // node_modules/@types/node - AutoImportProvider and main program
+ { path: "/node_modules/@types/node/package.json", content: `{ "name": "@types/node", "version": "1.0.0" }` },
+ { path: "/node_modules/@types/node/index.d.ts", content: `export declare class Stats {}` },
+
+ // root
+ { path: "/package.json", content: `{ "dependencies": { "memfs": "*" }, "devDependencies": { "@types/node": "*" } }` },
+ { path: "/tsconfig.json", content: `{ "compilerOptions": { "types": ["node"] }` },
+ { path: "/index.ts", content: `export {};` },
+ ];
+
+ const { projectService, session, triggerCompletions } = setup(files);
+ openFilesForSession([files[files.length - 1]], session);
+ const project = projectService.configuredProjects.get("/tsconfig.json")!;
+ const autoImportProvider = project.getPackageJsonAutoImportProvider()!;
+ assert.isDefined(autoImportProvider);
+
+ // Trigger completions to ensure export info map is populated
+ triggerCompletions("/index.ts", 0, 0);
+ const exportInfoMap = project.getCachedExportInfoMap();
+ const seenSymbolNames = new Set();
+ exportInfoMap.search("/index.ts" as ts.Path, /*preferCapitalized*/ false, ts.returnTrue, (info, symbolName) => {
+ assert.lengthOf(info, 1);
+ seenSymbolNames.add(symbolName);
+ });
+ assert.equal(seenSymbolNames.size, 2);
+ assert.ok(seenSymbolNames.has("Stats"));
+ assert.ok(seenSymbolNames.has("Volume"));
+ });
});
describe("unittests:: tsserver:: autoImportProvider - monorepo", () => {
@@ -333,7 +368,8 @@ function setup(files: File[]) {
projectService,
session,
updateFile,
- findAllReferences
+ findAllReferences,
+ triggerCompletions,
};
function updateFile(path: string, newText: string) {
@@ -360,4 +396,19 @@ function setup(files: File[]) {
}
});
}
+
+ function triggerCompletions(file: string, line: number, offset: number) {
+ const requestLocation: ts.server.protocol.FileLocationRequestArgs = {
+ file,
+ line,
+ offset,
+ };
+ session.executeCommandSeq({
+ command: ts.server.protocol.CommandTypes.CompletionInfo,
+ arguments: {
+ ...requestLocation,
+ includeExternalModuleExports: true,
+ }
+ });
+ }
}