1
1
// Used by importFixes to synthesize import module specifiers.
2
2
/* @internal */
3
3
namespace ts . moduleSpecifiers {
4
+ export interface ModuleSpecifierPreferences {
5
+ importModuleSpecifierPreference ?: "relative" | "non-relative" ;
6
+ }
7
+
4
8
// Note: fromSourceFile is just for usesJsExtensionOnImports
5
- export function getModuleSpecifier ( program : Program , fromSourceFile : SourceFile , fromSourceFileName : string , toFileName : string , host : LanguageServiceHost , preferences : UserPreferences ) {
6
- const info = getInfo ( program . getCompilerOptions ( ) , fromSourceFile , fromSourceFileName , host ) ;
7
- const compilerOptions = program . getCompilerOptions ( ) ;
9
+ export function getModuleSpecifier ( compilerOptions : CompilerOptions , fromSourceFile : SourceFile , fromSourceFileName : string , toFileName : string , host : ModuleSpecifierResolutionHost , preferences : ModuleSpecifierPreferences = { } ) {
10
+ const info = getInfo ( compilerOptions , fromSourceFile , fromSourceFileName , host ) ;
8
11
return getGlobalModuleSpecifier ( toFileName , info , host , compilerOptions ) ||
9
12
first ( getLocalModuleSpecifiers ( toFileName , info , compilerOptions , preferences ) ) ;
10
13
}
11
14
12
15
// For each symlink/original for a module, returns a list of ways to import that file.
13
16
export function getModuleSpecifiers (
14
17
moduleSymbol : Symbol ,
15
- program : Program ,
18
+ compilerOptions : CompilerOptions ,
16
19
importingSourceFile : SourceFile ,
17
- host : LanguageServiceHost ,
18
- preferences : UserPreferences ,
20
+ host : ModuleSpecifierResolutionHost ,
21
+ files : ReadonlyArray < SourceFile > ,
22
+ preferences : ModuleSpecifierPreferences ,
19
23
) : ReadonlyArray < ReadonlyArray < string > > {
20
24
const ambient = tryGetModuleNameFromAmbientModule ( moduleSymbol ) ;
21
25
if ( ambient ) return [ [ ambient ] ] ;
22
26
23
- const compilerOptions = program . getCompilerOptions ( ) ;
24
- const info = getInfo ( compilerOptions , importingSourceFile , importingSourceFile . fileName , host ) ;
25
- const modulePaths = getAllModulePaths ( program , moduleSymbol . valueDeclaration . getSourceFile ( ) ) ;
27
+ const info = getInfo ( compilerOptions , importingSourceFile , importingSourceFile . path , host ) ;
28
+ if ( ! files ) {
29
+ return Debug . fail ( "Files list must be present to resolve symlinks in specifier resolution" ) ;
30
+ }
31
+ const modulePaths = getAllModulePaths ( files , getSourceFileOfNode ( moduleSymbol . valueDeclaration ) , info . getCanonicalFileName , host ) ;
26
32
27
33
const global = mapDefined ( modulePaths , moduleFileName => getGlobalModuleSpecifier ( moduleFileName , info , host , compilerOptions ) ) ;
28
34
return global . length ? global . map ( g => [ g ] ) : modulePaths . map ( moduleFileName =>
@@ -36,18 +42,18 @@ namespace ts.moduleSpecifiers {
36
42
readonly sourceDirectory : string ;
37
43
}
38
44
// importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path
39
- function getInfo ( compilerOptions : CompilerOptions , importingSourceFile : SourceFile , importingSourceFileName : string , host : LanguageServiceHost ) : Info {
45
+ function getInfo ( compilerOptions : CompilerOptions , importingSourceFile : SourceFile , importingSourceFileName : string , host : ModuleSpecifierResolutionHost ) : Info {
40
46
const moduleResolutionKind = getEmitModuleResolutionKind ( compilerOptions ) ;
41
47
const addJsExtension = usesJsExtensionOnImports ( importingSourceFile ) ;
42
- const getCanonicalFileName = hostGetCanonicalFileName ( host ) ;
48
+ const getCanonicalFileName = createGetCanonicalFileName ( host . useCaseSensitiveFileNames ? host . useCaseSensitiveFileNames ( ) : true ) ;
43
49
const sourceDirectory = getDirectoryPath ( importingSourceFileName ) ;
44
50
return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory } ;
45
51
}
46
52
47
53
function getGlobalModuleSpecifier (
48
54
moduleFileName : string ,
49
55
{ addJsExtension, getCanonicalFileName, sourceDirectory } : Info ,
50
- host : LanguageServiceHost ,
56
+ host : ModuleSpecifierResolutionHost ,
51
57
compilerOptions : CompilerOptions ,
52
58
) {
53
59
return tryGetModuleNameFromTypeRoots ( compilerOptions , host , getCanonicalFileName , moduleFileName , addJsExtension )
@@ -59,7 +65,7 @@ namespace ts.moduleSpecifiers {
59
65
moduleFileName : string ,
60
66
{ moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory } : Info ,
61
67
compilerOptions : CompilerOptions ,
62
- preferences : UserPreferences ,
68
+ preferences : ModuleSpecifierPreferences ,
63
69
) {
64
70
const { baseUrl, paths } = compilerOptions ;
65
71
@@ -127,15 +133,57 @@ namespace ts.moduleSpecifiers {
127
133
return firstDefined ( imports , ( { text } ) => pathIsRelative ( text ) ? fileExtensionIs ( text , Extension . Js ) : undefined ) || false ;
128
134
}
129
135
136
+ function discoverProbableSymlinks ( files : ReadonlyArray < SourceFile > ) {
137
+ const symlinks = mapDefined ( files , sf =>
138
+ sf . resolvedModules && firstDefinedIterator ( sf . resolvedModules . values ( ) , res =>
139
+ res && res . originalPath && res . resolvedFileName !== res . originalPath ? [ res . resolvedFileName , res . originalPath ] : undefined ) ) ;
140
+ const result = createMap < string > ( ) ;
141
+ if ( symlinks ) {
142
+ for ( const [ resolvedPath , originalPath ] of symlinks ) {
143
+ const resolvedParts = getPathComponents ( resolvedPath ) ;
144
+ const originalParts = getPathComponents ( originalPath ) ;
145
+ while ( resolvedParts [ resolvedParts . length - 1 ] === originalParts [ originalParts . length - 1 ] ) {
146
+ resolvedParts . pop ( ) ;
147
+ originalParts . pop ( ) ;
148
+ }
149
+ result . set ( getPathFromPathComponents ( originalParts ) , getPathFromPathComponents ( resolvedParts ) ) ;
150
+ }
151
+ }
152
+ return result ;
153
+ }
154
+
155
+ function getAllModulePathsUsingIndirectSymlinks ( files : ReadonlyArray < SourceFile > , target : string , getCanonicalFileName : ( file : string ) => string , host : ModuleSpecifierResolutionHost ) {
156
+ const links = discoverProbableSymlinks ( files ) ;
157
+ const paths = arrayFrom ( links . keys ( ) ) ;
158
+ let options : string [ ] | undefined ;
159
+ for ( const path of paths ) {
160
+ const resolved = links . get ( path ) ! ;
161
+ if ( startsWith ( target , resolved + "/" ) ) {
162
+ const relative = getRelativePathFromDirectory ( resolved , target , getCanonicalFileName ) ;
163
+ const option = resolvePath ( path , relative ) ;
164
+ if ( ! host . fileExists || host . fileExists ( option ) ) {
165
+ if ( ! options ) options = [ ] ;
166
+ options . push ( option ) ;
167
+ }
168
+ }
169
+ }
170
+ const resolvedtarget = host . getCurrentDirectory ? resolvePath ( host . getCurrentDirectory ( ) , target ) : target ;
171
+ if ( options ) {
172
+ options . push ( resolvedtarget ) ; // Since these are speculative, we also include the original resolved name as a possibility
173
+ return options ;
174
+ }
175
+ return [ resolvedtarget ] ;
176
+ }
177
+
130
178
/**
131
179
* Looks for a existing imports that use symlinks to this module.
132
180
* Only if no symlink is available, the real path will be used.
133
181
*/
134
- function getAllModulePaths ( program : Program , { fileName } : SourceFile ) : ReadonlyArray < string > {
135
- const symlinks = mapDefined ( program . getSourceFiles ( ) , sf =>
182
+ function getAllModulePaths ( files : ReadonlyArray < SourceFile > , { fileName } : SourceFile , getCanonicalFileName : ( file : string ) => string , host : ModuleSpecifierResolutionHost ) : ReadonlyArray < string > {
183
+ const symlinks = mapDefined ( files , sf =>
136
184
sf . resolvedModules && firstDefinedIterator ( sf . resolvedModules . values ( ) , res =>
137
185
res && res . resolvedFileName === fileName ? res . originalPath : undefined ) ) ;
138
- return symlinks . length === 0 ? [ fileName ] : symlinks ;
186
+ return symlinks . length === 0 ? getAllModulePathsUsingIndirectSymlinks ( files , fileName , getCanonicalFileName , host ) : symlinks ;
139
187
}
140
188
141
189
function getRelativePathNParents ( relativePath : string ) : number {
@@ -210,7 +258,7 @@ namespace ts.moduleSpecifiers {
210
258
function tryGetModuleNameAsNodeModule (
211
259
options : CompilerOptions ,
212
260
moduleFileName : string ,
213
- host : LanguageServiceHost ,
261
+ host : ModuleSpecifierResolutionHost ,
214
262
getCanonicalFileName : ( file : string ) => string ,
215
263
sourceDirectory : string ,
216
264
) : string | undefined {
@@ -255,7 +303,8 @@ namespace ts.moduleSpecifiers {
255
303
const fullModulePathWithoutExtension = removeFileExtension ( path ) ;
256
304
257
305
// If the file is /index, it can be imported by its directory name
258
- if ( getCanonicalFileName ( fullModulePathWithoutExtension . substring ( parts . fileNameIndex ) ) === "/index" ) {
306
+ // IFF there is not _also_ a file by the same name
307
+ if ( getCanonicalFileName ( fullModulePathWithoutExtension . substring ( parts . fileNameIndex ) ) === "/index" && ! tryGetAnyFileFromPath ( host , fullModulePathWithoutExtension . substring ( 0 , parts . fileNameIndex ) ) ) {
259
308
return fullModulePathWithoutExtension . substring ( 0 , parts . fileNameIndex ) ;
260
309
}
261
310
@@ -274,6 +323,17 @@ namespace ts.moduleSpecifiers {
274
323
}
275
324
}
276
325
326
+ function tryGetAnyFileFromPath ( host : ModuleSpecifierResolutionHost , path : string ) {
327
+ // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
328
+ const extensions = getSupportedExtensions ( { allowJs : true } , [ { extension : "node" , isMixedContent : false } , { extension : "json" , isMixedContent : false , scriptKind : ScriptKind . JSON } ] ) ;
329
+ for ( const e of extensions ) {
330
+ const fullPath = path + e ;
331
+ if ( host . fileExists ! ( fullPath ) ) { // TODO: GH#18217
332
+ return fullPath ;
333
+ }
334
+ }
335
+ }
336
+
277
337
function getNodeModulePathParts ( fullPath : string ) {
278
338
// If fullPath can't be valid module file within node_modules, returns undefined.
279
339
// Example of expected pattern: /base/path/node_modules/[@scope /otherpackage/@otherscope /node_modules/]package/[subdirectory/]file.js
0 commit comments