@@ -555,6 +555,77 @@ namespace ts.moduleSpecifiers {
555
555
}
556
556
}
557
557
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
+
558
629
function tryGetModuleNameFromRootDirs ( rootDirs : readonly string [ ] , moduleFileName : string , sourceDirectory : string , getCanonicalFileName : ( file : string ) => string , ending : Ending , compilerOptions : CompilerOptions ) : string | undefined {
559
630
const normalizedTargetPath = getPathRelativeToRootDirs ( moduleFileName , rootDirs , getCanonicalFileName ) ;
560
631
if ( normalizedTargetPath === undefined ) {
@@ -586,7 +657,15 @@ namespace ts.moduleSpecifiers {
586
657
let moduleFileNameForExtensionless : string | undefined ;
587
658
while ( true ) {
588
659
// 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
+ }
590
669
if ( packageRootPath ) {
591
670
moduleSpecifier = packageRootPath ;
592
671
isPackageRootPath = true ;
@@ -621,12 +700,21 @@ namespace ts.moduleSpecifiers {
621
700
// For classic resolution, only allow importing from node_modules/@types, not other node_modules
622
701
return getEmitModuleResolutionKind ( options ) === ModuleResolutionKind . Classic && packageName === nodeModulesDirectoryName ? undefined : packageName ;
623
702
624
- function tryDirectoryWithPackageJson ( packageRootIndex : number ) {
703
+ function tryDirectoryWithPackageJson ( packageRootIndex : number ) : { moduleFileToTry : string , packageRootPath ?: string , blockedByExports ?: true , verbatimFromExports ?: true } {
625
704
const packageRootPath = path . substring ( 0 , packageRootIndex ) ;
626
705
const packageJsonPath = combinePaths ( packageRootPath , "package.json" ) ;
627
706
let moduleFileToTry = path ;
628
707
if ( host . fileExists ( packageJsonPath ) ) {
629
708
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
+ }
630
718
const versionPaths = packageJsonContent . typesVersions
631
719
? getPackageJsonTypesVersionsPaths ( packageJsonContent . typesVersions )
632
720
: undefined ;
@@ -641,7 +729,6 @@ namespace ts.moduleSpecifiers {
641
729
moduleFileToTry = combinePaths ( packageRootPath , fromPaths ) ;
642
730
}
643
731
}
644
-
645
732
// If the file is the main module, it can be imported by the package name
646
733
const mainFileRelative = packageJsonContent . typings || packageJsonContent . types || packageJsonContent . main ;
647
734
if ( isString ( mainFileRelative ) ) {
0 commit comments