@@ -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