@@ -121,7 +121,7 @@ namespace ts.moduleSpecifiers {
121
121
const info = getInfo ( importingSourceFileName , host ) ;
122
122
const modulePaths = getAllModulePaths ( importingSourceFileName , toFileName , host , userPreferences , options ) ;
123
123
return firstDefined ( modulePaths , modulePath => tryGetModuleNameAsNodeModule ( modulePath , info , importingSourceFile , host , compilerOptions , userPreferences , /*packageNameOnly*/ undefined , options . overrideImportMode ) ) ||
124
- getLocalModuleSpecifier ( toFileName , info , compilerOptions , host , preferences ) ;
124
+ getLocalModuleSpecifier ( toFileName , info , compilerOptions , host , options . overrideImportMode || importingSourceFile . impliedNodeFormat , preferences ) ;
125
125
}
126
126
127
127
export function tryGetModuleSpecifiersFromCache (
@@ -257,7 +257,7 @@ namespace ts.moduleSpecifiers {
257
257
}
258
258
259
259
if ( ! specifier && ! modulePath . isRedirect ) {
260
- const local = getLocalModuleSpecifier ( modulePath . path , info , compilerOptions , host , preferences ) ;
260
+ const local = getLocalModuleSpecifier ( modulePath . path , info , compilerOptions , host , options . overrideImportMode || importingSourceFile . impliedNodeFormat , preferences ) ;
261
261
if ( pathIsBareSpecifier ( local ) ) {
262
262
pathsSpecifiers = append ( pathsSpecifiers , local ) ;
263
263
}
@@ -293,7 +293,7 @@ namespace ts.moduleSpecifiers {
293
293
return { getCanonicalFileName, importingSourceFileName, sourceDirectory } ;
294
294
}
295
295
296
- function getLocalModuleSpecifier ( moduleFileName : string , info : Info , compilerOptions : CompilerOptions , host : ModuleSpecifierResolutionHost , { ending, relativePreference } : Preferences ) : string {
296
+ function getLocalModuleSpecifier ( moduleFileName : string , info : Info , compilerOptions : CompilerOptions , host : ModuleSpecifierResolutionHost , importMode : SourceFile [ "impliedNodeFormat" ] , { ending, relativePreference } : Preferences ) : string {
297
297
const { baseUrl, paths, rootDirs } = compilerOptions ;
298
298
const { sourceDirectory, getCanonicalFileName } = info ;
299
299
const relativePath = rootDirs && tryGetModuleNameFromRootDirs ( rootDirs , moduleFileName , sourceDirectory , getCanonicalFileName , ending , compilerOptions ) ||
@@ -308,9 +308,8 @@ namespace ts.moduleSpecifiers {
308
308
return relativePath ;
309
309
}
310
310
311
- const importRelativeToBaseUrl = removeExtensionAndIndexPostFix ( relativeToBaseUrl , ending , compilerOptions ) ;
312
- const fromPaths = paths && tryGetModuleNameFromPaths ( removeFileExtension ( relativeToBaseUrl ) , importRelativeToBaseUrl , paths ) ;
313
- const nonRelative = fromPaths === undefined && baseUrl !== undefined ? importRelativeToBaseUrl : fromPaths ;
311
+ const fromPaths = paths && tryGetModuleNameFromPaths ( relativeToBaseUrl , paths , getAllowedEndings ( ending , compilerOptions , importMode ) , host , compilerOptions ) ;
312
+ const nonRelative = fromPaths === undefined && baseUrl !== undefined ? removeExtensionAndIndexPostFix ( relativeToBaseUrl , ending , compilerOptions ) : fromPaths ;
314
313
if ( ! nonRelative ) {
315
314
return relativePath ;
316
315
}
@@ -559,27 +558,100 @@ namespace ts.moduleSpecifiers {
559
558
}
560
559
}
561
560
562
- function tryGetModuleNameFromPaths ( relativeToBaseUrlWithIndex : string , relativeToBaseUrl : string , paths : MapLike < readonly string [ ] > ) : string | undefined {
561
+ function getAllowedEndings ( preferredEnding : Ending , compilerOptions : CompilerOptions , importMode : SourceFile [ "impliedNodeFormat" ] ) {
562
+ if ( getEmitModuleResolutionKind ( compilerOptions ) >= ModuleResolutionKind . Node16 && importMode === ModuleKind . ESNext ) {
563
+ return [ Ending . JsExtension ] ;
564
+ }
565
+ switch ( preferredEnding ) {
566
+ case Ending . JsExtension : return [ Ending . JsExtension , Ending . Minimal , Ending . Index ] ;
567
+ case Ending . Index : return [ Ending . Index , Ending . Minimal , Ending . JsExtension ] ;
568
+ case Ending . Minimal : return [ Ending . Minimal , Ending . Index , Ending . JsExtension ] ;
569
+ default : Debug . assertNever ( preferredEnding ) ;
570
+ }
571
+ }
572
+
573
+ function tryGetModuleNameFromPaths ( relativeToBaseUrl : string , paths : MapLike < readonly string [ ] > , allowedEndings : Ending [ ] , host : ModuleSpecifierResolutionHost , compilerOptions : CompilerOptions ) : string | undefined {
563
574
for ( const key in paths ) {
564
575
for ( const patternText of paths [ key ] ) {
565
- const pattern = removeFileExtension ( normalizePath ( patternText ) ) ;
576
+ const pattern = normalizePath ( patternText ) ;
566
577
const indexOfStar = pattern . indexOf ( "*" ) ;
578
+ // In module resolution, if `pattern` itself has an extension, a file with that extension is looked up directly,
579
+ // meaning a '.ts' or '.d.ts' extension is allowed to resolve. This is distinct from the case where a '*' substitution
580
+ // causes a module specifier to have an extension, i.e. the extension comes from the module specifier in a JS/TS file
581
+ // and matches the '*'. For example:
582
+ //
583
+ // Module Specifier | Path Mapping (key: [pattern]) | Interpolation | Resolution Action
584
+ // ---------------------->------------------------------->--------------------->---------------------------------------------------------------
585
+ // import "@app/foo" -> "@app/*": ["./src/app/*.ts"] -> "./src/app/foo.ts" -> tryFile("./src/app/foo.ts") || [continue resolution algorithm]
586
+ // import "@app/foo.ts" -> "@app/*": ["./src/app/*"] -> "./src/app/foo.ts" -> [continue resolution algorithm]
587
+ //
588
+ // (https://github.com/microsoft/TypeScript/blob/ad4ded80e1d58f0bf36ac16bea71bc10d9f09895/src/compiler/moduleNameResolver.ts#L2509-L2516)
589
+ //
590
+ // The interpolation produced by both scenarios is identical, but only in the former, where the extension is encoded in
591
+ // the path mapping rather than in the module specifier, will we prioritize a file lookup on the interpolation result.
592
+ // (In fact, currently, the latter scenario will necessarily fail since no resolution mode recognizes '.ts' as a valid
593
+ // extension for a module specifier.)
594
+ //
595
+ // Here, this means we need to be careful about whether we generate a match from the target filename (typically with a
596
+ // .ts extension) or the possible relative module specifiers representing that file:
597
+ //
598
+ // Filename | Relative Module Specifier Candidates | Path Mapping | Filename Result | Module Specifier Results
599
+ // --------------------<----------------------------------------------<------------------------------<-------------------||----------------------------
600
+ // dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*.d.ts"] <- @app/haha || (none)
601
+ // dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*"] <- (none) || @app/haha, @app/haha.js
602
+ // dist/foo/index.d.ts <- dist/foo, dist/foo/index, dist/foo/index.js <- "@app/*": ["./dist/*.d.ts"] <- @app/foo/index || (none)
603
+ // dist/foo/index.d.ts <- dist/foo, dist/foo/index, dist/foo/index.js <- "@app/*": ["./dist/*"] <- (none) || @app/foo, @app/foo/index, @app/foo/index.js
604
+ // dist/wow.js.js <- dist/wow.js, dist/wow.js.js <- "@app/*": ["./dist/*.js"] <- @app/wow.js || @app/wow, @app/wow.js
605
+ //
606
+ // The "Filename Result" can be generated only if `pattern` has an extension. Care must be taken that the list of
607
+ // relative module specifiers to run the interpolation (a) is actually valid for the module resolution mode, (b) takes
608
+ // into account the existence of other files (e.g. 'dist/wow.js' cannot refer to 'dist/wow.js.js' if 'dist/wow.js'
609
+ // exists) and (c) that they are ordered by preference. The last row shows that the filename result and module
610
+ // specifier results are not mutually exclusive. Note that the filename result is a higher priority in module
611
+ // resolution, but as long criteria (b) above is met, I don't think its result needs to be the highest priority result
612
+ // in module specifier generation. I have included it last, as it's difficult to tell exactly where it should be
613
+ // sorted among the others for a particular value of `importModuleSpecifierEnding`.
614
+ const candidates : { ending : Ending | undefined , value : string } [ ] = allowedEndings . map ( ending => ( {
615
+ ending,
616
+ value : removeExtensionAndIndexPostFix ( relativeToBaseUrl , ending , compilerOptions )
617
+ } ) ) ;
618
+ if ( tryGetExtensionFromPath ( pattern ) ) {
619
+ candidates . push ( { ending : undefined , value : relativeToBaseUrl } ) ;
620
+ }
621
+
567
622
if ( indexOfStar !== - 1 ) {
568
- const prefix = pattern . substr ( 0 , indexOfStar ) ;
569
- const suffix = pattern . substr ( indexOfStar + 1 ) ;
570
- if ( relativeToBaseUrl . length >= prefix . length + suffix . length &&
571
- startsWith ( relativeToBaseUrl , prefix ) &&
572
- endsWith ( relativeToBaseUrl , suffix ) ||
573
- ! suffix && relativeToBaseUrl === removeTrailingDirectorySeparator ( prefix ) ) {
574
- const matchedStar = relativeToBaseUrl . substr ( prefix . length , relativeToBaseUrl . length - suffix . length - prefix . length ) ;
575
- return key . replace ( "*" , matchedStar ) ;
623
+ const prefix = pattern . substring ( 0 , indexOfStar ) ;
624
+ const suffix = pattern . substring ( indexOfStar + 1 ) ;
625
+ for ( const { ending, value } of candidates ) {
626
+ if ( value . length >= prefix . length + suffix . length &&
627
+ startsWith ( value , prefix ) &&
628
+ endsWith ( value , suffix ) &&
629
+ validateEnding ( { ending, value } )
630
+ ) {
631
+ const matchedStar = value . substring ( prefix . length , value . length - suffix . length ) ;
632
+ return key . replace ( "*" , matchedStar ) ;
633
+ }
576
634
}
577
635
}
578
- else if ( pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex ) {
636
+ else if (
637
+ some ( candidates , c => c . ending !== Ending . Minimal && pattern === c . value ) ||
638
+ some ( candidates , c => c . ending === Ending . Minimal && pattern === c . value && validateEnding ( c ) )
639
+ ) {
579
640
return key ;
580
641
}
581
642
}
582
643
}
644
+
645
+ function validateEnding ( { ending, value } : { ending : Ending | undefined , value : string } ) {
646
+ // Optimization: `removeExtensionAndIndexPostFix` can query the file system (a good bit) if `ending` is `Minimal`, the basename
647
+ // is 'index', and a `host` is provided. To avoid that until it's unavoidable, we ran the function with no `host` above. Only
648
+ // here, after we've checked that the minimal ending is indeed a match (via the length and prefix/suffix checks / `some` calls),
649
+ // do we check that the host-validated result is consistent with the answer we got before. If it's not, it falls back to the
650
+ // `Ending.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is
651
+ // that every module resolution mode that supports dropping extensions also supports dropping `/index`. Like literally
652
+ // everything else in this file, this logic needs to be updated if that's not true in some future module resolution mode.)
653
+ return ending !== Ending . Minimal || value === removeExtensionAndIndexPostFix ( relativeToBaseUrl , ending , compilerOptions , host ) ;
654
+ }
583
655
}
584
656
585
657
const enum MatchingMode {
@@ -677,10 +749,10 @@ namespace ts.moduleSpecifiers {
677
749
678
750
// Simplify the full file path to something that can be resolved by Node.
679
751
752
+ const preferences = getPreferences ( host , userPreferences , options , importingSourceFile ) ;
680
753
let moduleSpecifier = path ;
681
754
let isPackageRootPath = false ;
682
755
if ( ! packageNameOnly ) {
683
- const preferences = getPreferences ( host , userPreferences , options , importingSourceFile ) ;
684
756
let packageRootIndex = parts . packageRootIndex ;
685
757
let moduleFileName : string | undefined ;
686
758
while ( true ) {
@@ -732,15 +804,13 @@ namespace ts.moduleSpecifiers {
732
804
const packageRootPath = path . substring ( 0 , packageRootIndex ) ;
733
805
const packageJsonPath = combinePaths ( packageRootPath , "package.json" ) ;
734
806
let moduleFileToTry = path ;
807
+ let maybeBlockedByTypesVersions = false ;
735
808
const cachedPackageJson = host . getPackageJsonInfoCache ?.( ) ?. getPackageJsonInfo ( packageJsonPath ) ;
736
809
if ( typeof cachedPackageJson === "object" || cachedPackageJson === undefined && host . fileExists ( packageJsonPath ) ) {
737
810
const packageJsonContent = cachedPackageJson ?. packageJsonContent || JSON . parse ( host . readFile ! ( packageJsonPath ) ! ) ;
811
+ const importMode = overrideMode || importingSourceFile . impliedNodeFormat ;
738
812
if ( getEmitModuleResolutionKind ( options ) === ModuleResolutionKind . Node16 || getEmitModuleResolutionKind ( options ) === ModuleResolutionKind . NodeNext ) {
739
- // `conditions` *could* be made to go against `importingSourceFile.impliedNodeFormat` if something wanted to generate
740
- // an ImportEqualsDeclaration in an ESM-implied file or an ImportCall in a CJS-implied file. But since this function is
741
- // usually called to conjure an import out of thin air, we don't have an existing usage to call `getModeForUsageAtIndex`
742
- // with, so for now we just stick with the mode of the file.
743
- const conditions = [ "node" , overrideMode || importingSourceFile . impliedNodeFormat === ModuleKind . ESNext ? "import" : "require" , "types" ] ;
813
+ const conditions = [ "node" , importMode === ModuleKind . ESNext ? "import" : "require" , "types" ] ;
744
814
const fromExports = packageJsonContent . exports && typeof packageJsonContent . name === "string"
745
815
? tryGetModuleNameFromExports ( options , path , packageRootPath , getPackageNameFromTypesPackageName ( packageJsonContent . name ) , packageJsonContent . exports , conditions )
746
816
: undefined ;
@@ -760,19 +830,31 @@ namespace ts.moduleSpecifiers {
760
830
if ( versionPaths ) {
761
831
const subModuleName = path . slice ( packageRootPath . length + 1 ) ;
762
832
const fromPaths = tryGetModuleNameFromPaths (
763
- removeFileExtension ( subModuleName ) ,
764
- removeExtensionAndIndexPostFix ( subModuleName , Ending . Minimal , options ) ,
765
- versionPaths . paths
833
+ subModuleName ,
834
+ versionPaths . paths ,
835
+ getAllowedEndings ( preferences . ending , options , importMode ) ,
836
+ host ,
837
+ options
766
838
) ;
767
- if ( fromPaths !== undefined ) {
839
+ if ( fromPaths === undefined ) {
840
+ maybeBlockedByTypesVersions = true ;
841
+ }
842
+ else {
768
843
moduleFileToTry = combinePaths ( packageRootPath , fromPaths ) ;
769
844
}
770
845
}
771
846
// If the file is the main module, it can be imported by the package name
772
847
const mainFileRelative = packageJsonContent . typings || packageJsonContent . types || packageJsonContent . main || "index.js" ;
773
- if ( isString ( mainFileRelative ) ) {
848
+ if ( isString ( mainFileRelative ) && ! ( maybeBlockedByTypesVersions && matchPatternOrExact ( tryParsePatterns ( versionPaths ! . paths ) , mainFileRelative ) ) ) {
849
+ // The 'main' file is also subject to mapping through typesVersions, and we couldn't come up with a path
850
+ // explicitly through typesVersions, so if it matches a key in typesVersions now, it's not reachable.
851
+ // (The only way this can happen is if some file in a package that's not resolvable from outside the
852
+ // package got pulled into the program anyway, e.g. transitively through a file that *is* reachable. It
853
+ // happens very easily in fourslash tests though, since every test file listed gets included. See
854
+ // importNameCodeFix_typesVersions.ts for an example.)
774
855
const mainExportFile = toPath ( mainFileRelative , packageRootPath , getCanonicalFileName ) ;
775
856
if ( removeFileExtension ( mainExportFile ) === removeFileExtension ( getCanonicalFileName ( moduleFileToTry ) ) ) {
857
+ // ^ An arbitrary removal of file extension for this comparison is almost certainly wrong
776
858
return { packageRootPath, moduleFileToTry } ;
777
859
}
778
860
}
0 commit comments