@@ -368,9 +368,20 @@ namespace ts.Completions.StringCompletions {
368368 }
369369 }
370370
371+ function isEmitResolutionKindUsingNodeModules(compilerOptions: CompilerOptions): boolean {
372+ return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ||
373+ getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 ||
374+ getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
375+ }
376+
377+ function isEmitModuleResolutionRespectingExportMaps(compilerOptions: CompilerOptions) {
378+ return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 ||
379+ getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
380+ }
381+
371382 function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions): readonly Extension[][] {
372383 const extensions = getSupportedExtensions(compilerOptions);
373- return getEmitModuleResolutionKind (compilerOptions) === ModuleResolutionKind.NodeJs ?
384+ return isEmitResolutionKindUsingNodeModules (compilerOptions) ?
374385 getSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, extensions) :
375386 extensions;
376387 }
@@ -549,7 +560,7 @@ namespace ts.Completions.StringCompletions {
549560
550561 getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result);
551562
552- if (getEmitModuleResolutionKind (compilerOptions) === ModuleResolutionKind.NodeJs ) {
563+ if (isEmitResolutionKindUsingNodeModules (compilerOptions)) {
553564 // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
554565 // (But do if we didn't find anything, e.g. 'package.json' missing.)
555566 let foundGlobal = false;
@@ -562,12 +573,65 @@ namespace ts.Completions.StringCompletions {
562573 }
563574 }
564575 if (!foundGlobal) {
565- forEachAncestorDirectory(scriptPath, ancestor => {
576+ let ancestorLookup: (directory: string) => void | undefined = ancestor => {
566577 const nodeModules = combinePaths(ancestor, "node_modules");
567578 if (tryDirectoryExists(host, nodeModules)) {
568579 getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result);
569580 }
570- });
581+ };
582+ if (fragmentDirectory && isEmitModuleResolutionRespectingExportMaps(compilerOptions)) {
583+ const nodeModulesDirectoryLookup = ancestorLookup;
584+ ancestorLookup = ancestor => {
585+ const components = getPathComponents(fragment);
586+ components.shift(); // shift off empty root
587+ let packagePath = components.shift();
588+ if (!packagePath) {
589+ return nodeModulesDirectoryLookup(ancestor);
590+ }
591+ if (startsWith(packagePath, "@")) {
592+ const subName = components.shift();
593+ if (!subName) {
594+ return nodeModulesDirectoryLookup(ancestor);
595+ }
596+ packagePath = combinePaths(packagePath, subName);
597+ }
598+ const packageFile = combinePaths(ancestor, "node_modules", packagePath, "package.json");
599+ if (tryFileExists(host, packageFile)) {
600+ const packageJson = readJson(packageFile, host as { readFile: (filename: string) => string | undefined });
601+ const exports = (packageJson as any).exports;
602+ if (exports) {
603+ if (typeof exports !== "object" || exports === null) { // eslint-disable-line no-null/no-null
604+ return; // null exports or entrypoint only, no sub-modules available
605+ }
606+ const keys = getOwnKeys(exports);
607+ const fragmentSubpath = components.join("/");
608+ const processedKeys = mapDefined(keys, k => {
609+ if (k === ".") return undefined;
610+ if (!startsWith(k, "./")) return undefined;
611+ const subpath = k.substring(2);
612+ if (!startsWith(subpath, fragmentSubpath)) return undefined;
613+ // subpath is a valid export (barring conditions, which we don't currently check here)
614+ if (!stringContains(subpath, "*")) {
615+ return subpath;
616+ }
617+ // pattern export - only return everything up to the `*`, so the user can autocomplete, then
618+ // keep filling in the pattern (we could speculatively return a list of options by hitting disk,
619+ // but conditions will make that somewhat awkward, as each condition may have a different set of possible
620+ // options for the `*`.
621+ return subpath.slice(0, subpath.indexOf("*"));
622+ });
623+ forEach(processedKeys, k => {
624+ if (k) {
625+ result.push(nameAndKind(k, ScriptElementKind.externalModuleName, /*extension*/ undefined));
626+ }
627+ });
628+ return;
629+ }
630+ }
631+ return nodeModulesDirectoryLookup(ancestor);
632+ };
633+ }
634+ forEachAncestorDirectory(scriptPath, ancestorLookup);
571635 }
572636 }
573637
0 commit comments