@@ -555,6 +555,77 @@ namespace ts.moduleSpecifiers {
555555 }
556556 }
557557
558+ const enum MatchingMode {
559+ Exact ,
560+ Directory ,
561+ Pattern
562+ }
563+
564+ function tryGetModuleNameFromExports ( options : CompilerOptions , targetFilePath : string , packageDirectory : string , packageName : string , exports : unknown , conditions : string [ ] , mode = MatchingMode . Exact ) : { moduleFileToTry : string } | undefined {
565+ if ( typeof exports === "string" ) {
566+ const pathOrPattern = getNormalizedAbsolutePath ( combinePaths ( packageDirectory , exports ) , /*currentDirectory*/ undefined ) ;
567+ const extensionSwappedTarget = hasTSFileExtension ( targetFilePath ) ? removeFileExtension ( targetFilePath ) + tryGetJSExtensionForFile ( targetFilePath , options ) : undefined ;
568+ switch ( mode ) {
569+ case MatchingMode . Exact :
570+ if ( comparePaths ( targetFilePath , pathOrPattern ) === Comparison . EqualTo || ( extensionSwappedTarget && comparePaths ( extensionSwappedTarget , pathOrPattern ) === Comparison . EqualTo ) ) {
571+ return { moduleFileToTry : packageName } ;
572+ }
573+ break ;
574+ case MatchingMode . Directory :
575+ if ( containsPath ( pathOrPattern , targetFilePath ) ) {
576+ const fragment = getRelativePathFromDirectory ( pathOrPattern , targetFilePath , /*ignoreCase*/ false ) ;
577+ return { moduleFileToTry : getNormalizedAbsolutePath ( combinePaths ( combinePaths ( packageName , exports ) , fragment ) , /*currentDirectory*/ undefined ) } ;
578+ }
579+ break ;
580+ case MatchingMode . Pattern :
581+ const starPos = pathOrPattern . indexOf ( "*" ) ;
582+ const leadingSlice = pathOrPattern . slice ( 0 , starPos ) ;
583+ const trailingSlice = pathOrPattern . slice ( starPos + 1 ) ;
584+ if ( startsWith ( targetFilePath , leadingSlice ) && endsWith ( targetFilePath , trailingSlice ) ) {
585+ const starReplacement = targetFilePath . slice ( leadingSlice . length , targetFilePath . length - trailingSlice . length ) ;
586+ return { moduleFileToTry : packageName . replace ( "*" , starReplacement ) } ;
587+ }
588+ if ( extensionSwappedTarget && startsWith ( extensionSwappedTarget , leadingSlice ) && endsWith ( extensionSwappedTarget , trailingSlice ) ) {
589+ const starReplacement = extensionSwappedTarget . slice ( leadingSlice . length , extensionSwappedTarget . length - trailingSlice . length ) ;
590+ return { moduleFileToTry : packageName . replace ( "*" , starReplacement ) } ;
591+ }
592+ break ;
593+ }
594+ }
595+ else if ( Array . isArray ( exports ) ) {
596+ return forEach ( exports , e => tryGetModuleNameFromExports ( options , targetFilePath , packageDirectory , packageName , e , conditions ) ) ;
597+ }
598+ else if ( typeof exports === "object" && exports !== null ) { // eslint-disable-line no-null/no-null
599+ if ( allKeysStartWithDot ( exports as MapLike < unknown > ) ) {
600+ // sub-mappings
601+ // 3 cases:
602+ // * directory mappings (legacyish, key ends with / (technically allows index/extension resolution under cjs mode))
603+ // * pattern mappings (contains a *)
604+ // * exact mappings (no *, does not end with /)
605+ return forEach ( getOwnKeys ( exports as MapLike < unknown > ) , k => {
606+ const subPackageName = getNormalizedAbsolutePath ( combinePaths ( packageName , k ) , /*currentDirectory*/ undefined ) ;
607+ const mode = endsWith ( k , "/" ) ? MatchingMode . Directory
608+ : stringContains ( k , "*" ) ? MatchingMode . Pattern
609+ : MatchingMode . Exact ;
610+ return tryGetModuleNameFromExports ( options , targetFilePath , packageDirectory , subPackageName , ( exports as MapLike < unknown > ) [ k ] , conditions , mode ) ;
611+ } ) ;
612+ }
613+ else {
614+ // conditional mapping
615+ for ( const key of getOwnKeys ( exports as MapLike < unknown > ) ) {
616+ if ( key === "default" || conditions . indexOf ( key ) >= 0 || isApplicableVersionedTypesKey ( conditions , key ) ) {
617+ const subTarget = ( exports as MapLike < unknown > ) [ key ] ;
618+ const result = tryGetModuleNameFromExports ( options , targetFilePath , packageDirectory , packageName , subTarget , conditions ) ;
619+ if ( result ) {
620+ return result ;
621+ }
622+ }
623+ }
624+ }
625+ }
626+ return undefined ;
627+ }
628+
558629 function tryGetModuleNameFromRootDirs ( rootDirs : readonly string [ ] , moduleFileName : string , sourceDirectory : string , getCanonicalFileName : ( file : string ) => string , ending : Ending , compilerOptions : CompilerOptions ) : string | undefined {
559630 const normalizedTargetPath = getPathRelativeToRootDirs ( moduleFileName , rootDirs , getCanonicalFileName ) ;
560631 if ( normalizedTargetPath === undefined ) {
@@ -586,7 +657,15 @@ namespace ts.moduleSpecifiers {
586657 let moduleFileNameForExtensionless : string | undefined ;
587658 while ( true ) {
588659 // If the module could be imported by a directory name, use that directory's name
589- const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson ( packageRootIndex ) ;
660+ const { moduleFileToTry, packageRootPath, blockedByExports, verbatimFromExports } = tryDirectoryWithPackageJson ( packageRootIndex ) ;
661+ if ( getEmitModuleResolutionKind ( options ) !== ModuleResolutionKind . Classic ) {
662+ if ( blockedByExports ) {
663+ return undefined ; // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution
664+ }
665+ if ( verbatimFromExports ) {
666+ return moduleFileToTry ;
667+ }
668+ }
590669 if ( packageRootPath ) {
591670 moduleSpecifier = packageRootPath ;
592671 isPackageRootPath = true ;
@@ -621,12 +700,21 @@ namespace ts.moduleSpecifiers {
621700 // For classic resolution, only allow importing from node_modules/@types, not other node_modules
622701 return getEmitModuleResolutionKind ( options ) === ModuleResolutionKind . Classic && packageName === nodeModulesDirectoryName ? undefined : packageName ;
623702
624- function tryDirectoryWithPackageJson ( packageRootIndex : number ) {
703+ function tryDirectoryWithPackageJson ( packageRootIndex : number ) : { moduleFileToTry : string , packageRootPath ?: string , blockedByExports ?: true , verbatimFromExports ?: true } {
625704 const packageRootPath = path . substring ( 0 , packageRootIndex ) ;
626705 const packageJsonPath = combinePaths ( packageRootPath , "package.json" ) ;
627706 let moduleFileToTry = path ;
628707 if ( host . fileExists ( packageJsonPath ) ) {
629708 const packageJsonContent = JSON . parse ( host . readFile ! ( packageJsonPath ) ! ) ;
709+ // TODO: Inject `require` or `import` condition based on the intended import mode
710+ const fromExports = packageJsonContent . exports && typeof packageJsonContent . name === "string" ? tryGetModuleNameFromExports ( options , path , packageRootPath , packageJsonContent . name , packageJsonContent . exports , [ "node" , "types" ] ) : undefined ;
711+ if ( fromExports ) {
712+ const withJsExtension = ! hasTSFileExtension ( fromExports . moduleFileToTry ) ? fromExports : { moduleFileToTry : removeFileExtension ( fromExports . moduleFileToTry ) + tryGetJSExtensionForFile ( fromExports . moduleFileToTry , options ) } ;
713+ return { ...withJsExtension , verbatimFromExports : true } ;
714+ }
715+ if ( packageJsonContent . exports ) {
716+ return { moduleFileToTry : path , blockedByExports : true } ;
717+ }
630718 const versionPaths = packageJsonContent . typesVersions
631719 ? getPackageJsonTypesVersionsPaths ( packageJsonContent . typesVersions )
632720 : undefined ;
@@ -641,7 +729,6 @@ namespace ts.moduleSpecifiers {
641729 moduleFileToTry = combinePaths ( packageRootPath , fromPaths ) ;
642730 }
643731 }
644-
645732 // If the file is the main module, it can be imported by the package name
646733 const mainFileRelative = packageJsonContent . typings || packageJsonContent . types || packageJsonContent . main ;
647734 if ( isString ( mainFileRelative ) ) {
0 commit comments