1// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers. 2/* @internal */ 3namespace ts.moduleSpecifiers { 4 const enum RelativePreference { Relative, NonRelative, Shortest, ExternalNonRelative } 5 // See UserPreferences#importPathEnding 6 const enum Ending { Minimal, Index, JsExtension } 7 8 // Processed preferences 9 interface Preferences { 10 readonly relativePreference: RelativePreference; 11 readonly ending: Ending; 12 } 13 14 function getPreferences(host: ModuleSpecifierResolutionHost, { importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences { 15 return { 16 relativePreference: 17 importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : 18 importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : 19 importModuleSpecifierPreference === "project-relative" ? RelativePreference.ExternalNonRelative : 20 RelativePreference.Shortest, 21 ending: getEnding(), 22 }; 23 function getEnding(): Ending { 24 switch (importModuleSpecifierEnding) { 25 case "minimal": return Ending.Minimal; 26 case "index": return Ending.Index; 27 case "js": return Ending.JsExtension; 28 default: return usesJsExtensionOnImports(importingSourceFile) || isFormatRequiringExtensions(compilerOptions, importingSourceFile.path, host) ? Ending.JsExtension 29 : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; 30 } 31 } 32 } 33 34 function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string, importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Preferences { 35 return { 36 relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, 37 ending: hasJSFileExtension(oldImportSpecifier) || isFormatRequiringExtensions(compilerOptions, importingSourceFileName, host) ? 38 Ending.JsExtension : 39 getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, 40 }; 41 } 42 43 function isFormatRequiringExtensions(compilerOptions: CompilerOptions, importingSourceFileName: Path, host: ModuleSpecifierResolutionHost) { 44 if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node16 45 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) { 46 return false; 47 } 48 return getImpliedNodeFormatForFile(importingSourceFileName, host.getPackageJsonInfoCache?.(), getModuleResolutionHost(host), compilerOptions) !== ModuleKind.CommonJS; 49 } 50 51 function getModuleResolutionHost(host: ModuleSpecifierResolutionHost): ModuleResolutionHost { 52 return { 53 fileExists: host.fileExists, 54 readFile: Debug.checkDefined(host.readFile), 55 directoryExists: host.directoryExists, 56 getCurrentDirectory: host.getCurrentDirectory, 57 realpath: host.realpath, 58 useCaseSensitiveFileNames: host.useCaseSensitiveFileNames?.(), 59 }; 60 } 61 62 // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? 63 // Because when this is called by the file renamer, `importingSourceFile` is the file being renamed, 64 // while `importingSourceFileName` its *new* name. We need a source file just to get its 65 // `impliedNodeFormat` and to detect certain preferences from existing import module specifiers. 66 export function updateModuleSpecifier( 67 compilerOptions: CompilerOptions, 68 importingSourceFile: SourceFile, 69 importingSourceFileName: Path, 70 toFileName: string, 71 host: ModuleSpecifierResolutionHost, 72 oldImportSpecifier: string, 73 options: ModuleSpecifierOptions = {}, 74 ): string | undefined { 75 const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}, options); 76 if (res === oldImportSpecifier) return undefined; 77 return res; 78 } 79 80 // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? 81 // Because when this is called by the declaration emitter, `importingSourceFile` is the implementation 82 // file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the 83 // one currently being produced; the latter to the one being imported). We need an implementation file 84 // just to get its `impliedNodeFormat` and to detect certain preferences from existing import module 85 // specifiers. 86 export function getModuleSpecifier( 87 compilerOptions: CompilerOptions, 88 importingSourceFile: SourceFile, 89 importingSourceFileName: Path, 90 toFileName: string, 91 host: ModuleSpecifierResolutionHost, 92 options: ModuleSpecifierOptions = {}, 93 ): string { 94 return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}, options); 95 } 96 97 export function getNodeModulesPackageName( 98 compilerOptions: CompilerOptions, 99 importingSourceFile: SourceFile, 100 nodeModulesFileName: string, 101 host: ModuleSpecifierResolutionHost, 102 preferences: UserPreferences, 103 options: ModuleSpecifierOptions = {}, 104 ): string | undefined { 105 const info = getInfo(importingSourceFile.path, host); 106 const modulePaths = getAllModulePaths(importingSourceFile.path, nodeModulesFileName, host, preferences, options, compilerOptions.packageManagerType); 107 return firstDefined(modulePaths, 108 modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, preferences, /*packageNameOnly*/ true, options.overrideImportMode)); 109 } 110 111 function getModuleSpecifierWorker( 112 compilerOptions: CompilerOptions, 113 importingSourceFile: SourceFile, 114 importingSourceFileName: Path, 115 toFileName: string, 116 host: ModuleSpecifierResolutionHost, 117 preferences: Preferences, 118 userPreferences: UserPreferences, 119 options: ModuleSpecifierOptions = {} 120 ): string { 121 const info = getInfo(importingSourceFileName, host); 122 const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host, userPreferences, options, compilerOptions.packageManagerType); 123 return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode)) || 124 getLocalModuleSpecifier(toFileName, info, compilerOptions, host, options.overrideImportMode || importingSourceFile.impliedNodeFormat, preferences); 125 } 126 127 export function tryGetModuleSpecifiersFromCache( 128 moduleSymbol: Symbol, 129 importingSourceFile: SourceFile, 130 host: ModuleSpecifierResolutionHost, 131 userPreferences: UserPreferences, 132 options: ModuleSpecifierOptions = {}, 133 ): readonly string[] | undefined { 134 return tryGetModuleSpecifiersFromCacheWorker( 135 moduleSymbol, 136 importingSourceFile, 137 host, 138 userPreferences, 139 options)[0]; 140 } 141 142 function tryGetModuleSpecifiersFromCacheWorker( 143 moduleSymbol: Symbol, 144 importingSourceFile: SourceFile, 145 host: ModuleSpecifierResolutionHost, 146 userPreferences: UserPreferences, 147 options: ModuleSpecifierOptions = {}, 148 ): readonly [specifiers?: readonly string[], moduleFile?: SourceFile, modulePaths?: readonly ModulePath[], cache?: ModuleSpecifierCache] { 149 const moduleSourceFile = getSourceFileOfModule(moduleSymbol); 150 if (!moduleSourceFile) { 151 return emptyArray as []; 152 } 153 154 const cache = host.getModuleSpecifierCache?.(); 155 const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences, options); 156 return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache]; 157 } 158 159 /** Returns an import for each symlink and for the realpath. */ 160 export function getModuleSpecifiers( 161 moduleSymbol: Symbol, 162 checker: TypeChecker, 163 compilerOptions: CompilerOptions, 164 importingSourceFile: SourceFile, 165 host: ModuleSpecifierResolutionHost, 166 userPreferences: UserPreferences, 167 options: ModuleSpecifierOptions = {}, 168 ): readonly string[] { 169 return getModuleSpecifiersWithCacheInfo( 170 moduleSymbol, 171 checker, 172 compilerOptions, 173 importingSourceFile, 174 host, 175 userPreferences, 176 options 177 ).moduleSpecifiers; 178 } 179 180 export function getModuleSpecifiersWithCacheInfo( 181 moduleSymbol: Symbol, 182 checker: TypeChecker, 183 compilerOptions: CompilerOptions, 184 importingSourceFile: SourceFile, 185 host: ModuleSpecifierResolutionHost, 186 userPreferences: UserPreferences, 187 options: ModuleSpecifierOptions = {}, 188 ): { moduleSpecifiers: readonly string[], computedWithoutCache: boolean } { 189 let computedWithoutCache = false; 190 const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker); 191 if (ambient) return { moduleSpecifiers: [ambient], computedWithoutCache }; 192 193 // eslint-disable-next-line prefer-const 194 let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker( 195 moduleSymbol, 196 importingSourceFile, 197 host, 198 userPreferences, 199 options 200 ); 201 if (specifiers) return { moduleSpecifiers: specifiers, computedWithoutCache }; 202 if (!moduleSourceFile) return { moduleSpecifiers: emptyArray, computedWithoutCache }; 203 204 computedWithoutCache = true; 205 modulePaths ||= getAllModulePathsWorker(importingSourceFile.path, moduleSourceFile.originalFileName, host, compilerOptions.packageManagerType); 206 const result = computeModuleSpecifiers(modulePaths, compilerOptions, importingSourceFile, host, userPreferences, options); 207 cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, options, modulePaths, result); 208 return { moduleSpecifiers: result, computedWithoutCache }; 209 } 210 211 function computeModuleSpecifiers( 212 modulePaths: readonly ModulePath[], 213 compilerOptions: CompilerOptions, 214 importingSourceFile: SourceFile, 215 host: ModuleSpecifierResolutionHost, 216 userPreferences: UserPreferences, 217 options: ModuleSpecifierOptions = {}, 218 ): readonly string[] { 219 const info = getInfo(importingSourceFile.path, host); 220 const preferences = getPreferences(host, userPreferences, compilerOptions, importingSourceFile); 221 const existingSpecifier = forEach(modulePaths, modulePath => forEach( 222 host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), 223 reason => { 224 if (reason.kind !== FileIncludeKind.Import || reason.file !== importingSourceFile.path) return undefined; 225 // If the candidate import mode doesn't match the mode we're generating for, don't consider it 226 // TODO: maybe useful to keep around as an alternative option for certain contexts where the mode is overridable 227 if (importingSourceFile.impliedNodeFormat && importingSourceFile.impliedNodeFormat !== getModeForResolutionAtIndex(importingSourceFile, reason.index)) return undefined; 228 const specifier = getModuleNameStringLiteralAt(importingSourceFile, reason.index).text; 229 // If the preference is for non relative and the module specifier is relative, ignore it 230 return preferences.relativePreference !== RelativePreference.NonRelative || !pathIsRelative(specifier) ? 231 specifier : 232 undefined; 233 } 234 )); 235 if (existingSpecifier) { 236 const moduleSpecifiers = [existingSpecifier]; 237 return moduleSpecifiers; 238 } 239 240 const importedFileIsInNodeModules = some(modulePaths, p => p.isInNodeModules); 241 242 // Module specifier priority: 243 // 1. "Bare package specifiers" (e.g. "@foo/bar") resulting from a path through node_modules or oh_modules to a package.json's or oh-package.json5's "types" entry 244 // 2. Specifiers generated using "paths" from tsconfig 245 // 3. Non-relative specfiers resulting from a path through node_modules or oh_modules(e.g. "@foo/bar/path/to/file") 246 // 4. Relative paths 247 let modulesSpecifiers: string[] | undefined; 248 let pathsSpecifiers: string[] | undefined; 249 let relativeSpecifiers: string[] | undefined; 250 for (const modulePath of modulePaths) { 251 const specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, /*packageNameOnly*/ undefined, options.overrideImportMode); 252 modulesSpecifiers = append(modulesSpecifiers, specifier); 253 if (specifier && modulePath.isRedirect) { 254 // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", 255 // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking. 256 return modulesSpecifiers!; 257 } 258 259 if (!specifier && !modulePath.isRedirect) { 260 const local = getLocalModuleSpecifier(modulePath.path, info, compilerOptions, host, options.overrideImportMode || importingSourceFile.impliedNodeFormat, preferences); 261 if (pathIsBareSpecifier(local)) { 262 pathsSpecifiers = append(pathsSpecifiers, local); 263 } 264 else if (!importedFileIsInNodeModules || modulePath.isInNodeModules) { 265 // Why this extra conditional, not just an `else`? If some path to the file contained 266 // 'node_modules' or 'oh_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"), 267 // that means we had to go through a *sibling's* node_modules or oh_modules, not one we can access directly. 268 // If some path to the file was in node_modules or oh_modules but another was not, this likely indicates that 269 // we have a monorepo structure with symlinks. In this case, the non-node_modules or oh_modules path is 270 // probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package 271 // in a monorepo is probably not portable. So, the module specifier we actually go with will be 272 // the relative path through node_modules or oh_modules, so that the declaration emitter can produce a 273 // portability error. (See declarationEmitReexportedSymlinkReference3) 274 relativeSpecifiers = append(relativeSpecifiers, local); 275 } 276 } 277 } 278 279 return pathsSpecifiers?.length ? pathsSpecifiers : 280 modulesSpecifiers?.length ? modulesSpecifiers : 281 Debug.checkDefined(relativeSpecifiers); 282 } 283 284 interface Info { 285 readonly getCanonicalFileName: GetCanonicalFileName; 286 readonly importingSourceFileName: Path 287 readonly sourceDirectory: Path; 288 } 289 // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path 290 function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info { 291 const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); 292 const sourceDirectory = getDirectoryPath(importingSourceFileName); 293 return { getCanonicalFileName, importingSourceFileName, sourceDirectory }; 294 } 295 296 function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: SourceFile["impliedNodeFormat"], { ending, relativePreference }: Preferences): string { 297 const { baseUrl, paths, rootDirs } = compilerOptions; 298 const { sourceDirectory, getCanonicalFileName } = info; 299 const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || 300 removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); 301 if (!baseUrl && !paths || relativePreference === RelativePreference.Relative) { 302 return relativePath; 303 } 304 305 const baseDirectory = getNormalizedAbsolutePath(getPathsBasePath(compilerOptions, host) || baseUrl!, host.getCurrentDirectory()); 306 const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseDirectory, getCanonicalFileName); 307 if (!relativeToBaseUrl) { 308 return relativePath; 309 } 310 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; 313 if (!nonRelative) { 314 return relativePath; 315 } 316 317 if (relativePreference === RelativePreference.NonRelative) { 318 return nonRelative; 319 } 320 321 if (relativePreference === RelativePreference.ExternalNonRelative) { 322 const projectDirectory = compilerOptions.configFilePath ? 323 toPath(getDirectoryPath(compilerOptions.configFilePath), host.getCurrentDirectory(), info.getCanonicalFileName) : 324 info.getCanonicalFileName(host.getCurrentDirectory()); 325 const modulePath = toPath(moduleFileName, projectDirectory, getCanonicalFileName); 326 const sourceIsInternal = startsWith(sourceDirectory, projectDirectory); 327 const targetIsInternal = startsWith(modulePath, projectDirectory); 328 if (sourceIsInternal && !targetIsInternal || !sourceIsInternal && targetIsInternal) { 329 // 1. The import path crosses the boundary of the tsconfig.json-containing directory. 330 // 331 // src/ 332 // tsconfig.json 333 // index.ts ------- 334 // lib/ | (path crosses tsconfig.json) 335 // imported.ts <--- 336 // 337 return nonRelative; 338 } 339 const packageManagerType = compilerOptions.packageManagerType; 340 const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, getDirectoryPath(modulePath), packageManagerType); 341 const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory, packageManagerType); 342 if (nearestSourcePackageJson !== nearestTargetPackageJson) { 343 // 2. The importing and imported files are part of different packages. 344 // 345 // packages/a/ 346 // package.json 347 // index.ts -------- 348 // packages/b/ | (path crosses package.json) 349 // package.json | 350 // component.ts <--- 351 // 352 return nonRelative; 353 } 354 355 return relativePath; 356 } 357 358 if (relativePreference !== RelativePreference.Shortest) Debug.assertNever(relativePreference); 359 360 // Prefer a relative import over a baseUrl import if it has fewer components. 361 return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative; 362 } 363 364 export function countPathComponents(path: string): number { 365 let count = 0; 366 for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) { 367 if (path.charCodeAt(i) === CharacterCodes.slash) count++; 368 } 369 return count; 370 } 371 372 function usesJsExtensionOnImports({ imports }: SourceFile): boolean { 373 return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false; 374 } 375 376 function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) { 377 return compareBooleans(b.isRedirect, a.isRedirect) || compareNumberOfDirectorySeparators(a.path, b.path); 378 } 379 380 function getNearestAncestorDirectoryWithPackageJson(host: ModuleSpecifierResolutionHost, fileName: string, packageManagerType?: string) { 381 if (host.getNearestAncestorDirectoryWithPackageJson) { 382 return host.getNearestAncestorDirectoryWithPackageJson(fileName); 383 } 384 return !!forEachAncestorDirectory(fileName, directory => { 385 return host.fileExists(combinePaths(directory, getPackageJsonByPMType(packageManagerType))) ? true : undefined; 386 }); 387 } 388 389 export function forEachFileNameOfModule<T>( 390 importingFileName: string, 391 importedFileName: string, 392 host: ModuleSpecifierResolutionHost, 393 preferSymlinks: boolean, 394 cb: (fileName: string, isRedirect: boolean) => T | undefined 395 ): T | undefined { 396 const getCanonicalFileName = hostGetCanonicalFileName(host); 397 const cwd = host.getCurrentDirectory(); 398 const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined; 399 const importedPath = toPath(importedFileName, cwd, getCanonicalFileName); 400 const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray; 401 const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects]; 402 const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd)); 403 let shouldFilterIgnoredPaths = !every(targets, containsIgnoredPath); 404 405 if (!preferSymlinks) { 406 // Symlinks inside ignored paths are already filtered out of the symlink cache, 407 // so we only need to remove them from the realpath filenames. 408 const result = forEach(targets, p => !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) && cb(p, referenceRedirect === p)); 409 if (result) return result; 410 } 411 412 const symlinkedDirectories = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath(); 413 const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd); 414 const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => { 415 const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName))); 416 if (!symlinkDirectories) return undefined; // Continue to ancestor directory 417 418 // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) 419 if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { 420 return false; // Stop search, each ancestor directory will also hit this condition 421 } 422 423 return forEach(targets, target => { 424 if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { 425 return; 426 } 427 428 const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); 429 for (const symlinkDirectory of symlinkDirectories) { 430 const option = resolvePath(symlinkDirectory, relative); 431 const result = cb(option, target === referenceRedirect); 432 shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths 433 if (result) return result; 434 } 435 }); 436 }); 437 return result || (preferSymlinks 438 ? forEach(targets, p => shouldFilterIgnoredPaths && containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect)) 439 : undefined); 440 } 441 442 /** 443 * Looks for existing imports that use symlinks to this module. 444 * Symlinks will be returned first so they are preferred over the real path. 445 */ 446 function getAllModulePaths( 447 importingFilePath: Path, 448 importedFileName: string, 449 host: ModuleSpecifierResolutionHost, 450 preferences: UserPreferences, 451 options: ModuleSpecifierOptions = {}, 452 packageManagerType?: string 453 ) { 454 const importedFilePath = toPath(importedFileName, host.getCurrentDirectory(), hostGetCanonicalFileName(host)); 455 const cache = host.getModuleSpecifierCache?.(); 456 if (cache) { 457 const cached = cache.get(importingFilePath, importedFilePath, preferences, options); 458 if (cached?.modulePaths) return cached.modulePaths; 459 } 460 const modulePaths = getAllModulePathsWorker(importingFilePath, importedFileName, host, packageManagerType); 461 if (cache) { 462 cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths); 463 } 464 return modulePaths; 465 } 466 467 function getAllModulePathsWorker(importingFileName: Path, importedFileName: string, host: ModuleSpecifierResolutionHost, packageManagerType?: string): readonly ModulePath[] { 468 const getCanonicalFileName = hostGetCanonicalFileName(host); 469 const allFileNames = new Map<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>(); 470 let importedFileFromNodeModules = false; 471 forEachFileNameOfModule( 472 importingFileName, 473 importedFileName, 474 host, 475 /*preferSymlinks*/ true, 476 (path, isRedirect) => { 477 const isInNodeModules = isOhpm(packageManagerType) ? pathContainsOHModules(path) : pathContainsNodeModules(path); 478 allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules }); 479 importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules; 480 // don't return value, so we collect everything 481 } 482 ); 483 484 // Sort by paths closest to importing file Name directory 485 const sortedPaths: ModulePath[] = []; 486 for ( 487 let directory = getDirectoryPath(importingFileName); 488 allFileNames.size !== 0; 489 ) { 490 const directoryStart = ensureTrailingDirectorySeparator(directory); 491 let pathsInDirectory: ModulePath[] | undefined; 492 allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => { 493 if (startsWith(path, directoryStart)) { 494 (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules }); 495 allFileNames.delete(fileName); 496 } 497 }); 498 if (pathsInDirectory) { 499 if (pathsInDirectory.length > 1) { 500 pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); 501 } 502 sortedPaths.push(...pathsInDirectory); 503 } 504 const newDirectory = getDirectoryPath(directory); 505 if (newDirectory === directory) break; 506 directory = newDirectory; 507 } 508 if (allFileNames.size) { 509 const remainingPaths = arrayFrom(allFileNames.values()); 510 if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); 511 sortedPaths.push(...remainingPaths); 512 } 513 514 return sortedPaths; 515 } 516 517 function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeChecker): string | undefined { 518 const decl = moduleSymbol.declarations?.find( 519 d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name))) 520 ) as (ModuleDeclaration & { name: StringLiteral }) | undefined; 521 if (decl) { 522 return decl.name.text; 523 } 524 525 // the module could be a namespace, which is export through "export=" from an ambient module. 526 /** 527 * declare module "m" { 528 * namespace ns { 529 * class c {} 530 * } 531 * export = ns; 532 * } 533 */ 534 // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m" 535 const ambientModuleDeclareCandidates = mapDefined(moduleSymbol.declarations, 536 d => { 537 if (!isModuleDeclaration(d)) return; 538 const topNamespace = getTopNamespace(d); 539 if (!(topNamespace?.parent?.parent 540 && isModuleBlock(topNamespace.parent) && isAmbientModule(topNamespace.parent.parent) && isSourceFile(topNamespace.parent.parent.parent))) return; 541 const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as __String)?.valueDeclaration as ExportAssignment)?.expression as PropertyAccessExpression | Identifier); 542 if (!exportAssignment) return; 543 const exportSymbol = checker.getSymbolAtLocation(exportAssignment); 544 if (!exportSymbol) return; 545 const originalExportSymbol = exportSymbol?.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol; 546 if (originalExportSymbol === d.symbol) return topNamespace.parent.parent; 547 548 function getTopNamespace(namespaceDeclaration: ModuleDeclaration) { 549 while (namespaceDeclaration.flags & NodeFlags.NestedNamespace) { 550 namespaceDeclaration = namespaceDeclaration.parent as ModuleDeclaration; 551 } 552 return namespaceDeclaration; 553 } 554 } 555 ); 556 const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (AmbientModuleDeclaration & { name: StringLiteral }) | undefined; 557 if (ambientModuleDeclare) { 558 return ambientModuleDeclare.name.text; 559 } 560 } 561 562 function getAllowedEndings(preferredEnding: Ending, compilerOptions: CompilerOptions, importMode: SourceFile["impliedNodeFormat"]) { 563 if (getEmitModuleResolutionKind(compilerOptions) >= ModuleResolutionKind.Node16 && importMode === ModuleKind.ESNext) { 564 return [Ending.JsExtension]; 565 } 566 switch (preferredEnding) { 567 case Ending.JsExtension: return [Ending.JsExtension, Ending.Minimal, Ending.Index]; 568 case Ending.Index: return [Ending.Index, Ending.Minimal, Ending.JsExtension]; 569 case Ending.Minimal: return [Ending.Minimal, Ending.Index, Ending.JsExtension]; 570 default: Debug.assertNever(preferredEnding); 571 } 572 } 573 574 function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<readonly string[]>, allowedEndings: Ending[], host: ModuleSpecifierResolutionHost, compilerOptions: CompilerOptions): string | undefined { 575 for (const key in paths) { 576 for (const patternText of paths[key]) { 577 const pattern = normalizePath(patternText); 578 const indexOfStar = pattern.indexOf("*"); 579 // In module resolution, if `pattern` itself has an extension, a file with that extension is looked up directly, 580 // meaning a '.ts' or '.d.ts' extension is allowed to resolve. This is distinct from the case where a '*' substitution 581 // causes a module specifier to have an extension, i.e. the extension comes from the module specifier in a JS/TS file 582 // and matches the '*'. For example: 583 // 584 // Module Specifier | Path Mapping (key: [pattern]) | Interpolation | Resolution Action 585 // ---------------------->------------------------------->--------------------->--------------------------------------------------------------- 586 // import "@app/foo" -> "@app/*": ["./src/app/*.ts"] -> "./src/app/foo.ts" -> tryFile("./src/app/foo.ts") || [continue resolution algorithm] 587 // import "@app/foo.ts" -> "@app/*": ["./src/app/*"] -> "./src/app/foo.ts" -> [continue resolution algorithm] 588 // 589 // (https://github.com/microsoft/TypeScript/blob/ad4ded80e1d58f0bf36ac16bea71bc10d9f09895/src/compiler/moduleNameResolver.ts#L2509-L2516) 590 // 591 // The interpolation produced by both scenarios is identical, but only in the former, where the extension is encoded in 592 // the path mapping rather than in the module specifier, will we prioritize a file lookup on the interpolation result. 593 // (In fact, currently, the latter scenario will necessarily fail since no resolution mode recognizes '.ts' as a valid 594 // extension for a module specifier.) 595 // 596 // Here, this means we need to be careful about whether we generate a match from the target filename (typically with a 597 // .ts extension) or the possible relative module specifiers representing that file: 598 // 599 // Filename | Relative Module Specifier Candidates | Path Mapping | Filename Result | Module Specifier Results 600 // --------------------<----------------------------------------------<------------------------------<-------------------||---------------------------- 601 // dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*.d.ts"] <- @app/haha || (none) 602 // dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*"] <- (none) || @app/haha, @app/haha.js 603 // dist/foo/index.d.ts <- dist/foo, dist/foo/index, dist/foo/index.js <- "@app/*": ["./dist/*.d.ts"] <- @app/foo/index || (none) 604 // 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 605 // dist/wow.js.js <- dist/wow.js, dist/wow.js.js <- "@app/*": ["./dist/*.js"] <- @app/wow.js || @app/wow, @app/wow.js 606 // 607 // The "Filename Result" can be generated only if `pattern` has an extension. Care must be taken that the list of 608 // relative module specifiers to run the interpolation (a) is actually valid for the module resolution mode, (b) takes 609 // into account the existence of other files (e.g. 'dist/wow.js' cannot refer to 'dist/wow.js.js' if 'dist/wow.js' 610 // exists) and (c) that they are ordered by preference. The last row shows that the filename result and module 611 // specifier results are not mutually exclusive. Note that the filename result is a higher priority in module 612 // resolution, but as long criteria (b) above is met, I don't think its result needs to be the highest priority result 613 // in module specifier generation. I have included it last, as it's difficult to tell exactly where it should be 614 // sorted among the others for a particular value of `importModuleSpecifierEnding`. 615 const candidates: { ending: Ending | undefined, value: string }[] = allowedEndings.map(ending => ({ 616 ending, 617 value: removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions) 618 })); 619 if (tryGetExtensionFromPath(pattern)) { 620 candidates.push({ ending: undefined, value: relativeToBaseUrl }); 621 } 622 623 if (indexOfStar !== -1) { 624 const prefix = pattern.substring(0, indexOfStar); 625 const suffix = pattern.substring(indexOfStar + 1); 626 for (const { ending, value } of candidates) { 627 if (value.length >= prefix.length + suffix.length && 628 startsWith(value, prefix) && 629 endsWith(value, suffix) && 630 validateEnding({ ending, value }) 631 ) { 632 const matchedStar = value.substring(prefix.length, value.length - suffix.length); 633 return key.replace("*", matchedStar); 634 } 635 } 636 } 637 else if ( 638 some(candidates, c => c.ending !== Ending.Minimal && pattern === c.value) || 639 some(candidates, c => c.ending === Ending.Minimal && pattern === c.value && validateEnding(c)) 640 ) { 641 return key; 642 } 643 } 644 } 645 646 function validateEnding({ ending, value }: { ending: Ending | undefined, value: string }) { 647 // Optimization: `removeExtensionAndIndexPostFix` can query the file system (a good bit) if `ending` is `Minimal`, the basename 648 // is 'index', and a `host` is provided. To avoid that until it's unavoidable, we ran the function with no `host` above. Only 649 // here, after we've checked that the minimal ending is indeed a match (via the length and prefix/suffix checks / `some` calls), 650 // 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 651 // `Ending.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is 652 // that every module resolution mode that supports dropping extensions also supports dropping `/index`. Like literally 653 // everything else in this file, this logic needs to be updated if that's not true in some future module resolution mode.) 654 return ending !== Ending.Minimal || value === removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions, host); 655 } 656 } 657 658 const enum MatchingMode { 659 Exact, 660 Directory, 661 Pattern 662 } 663 664 function tryGetModuleNameFromExports(options: CompilerOptions, targetFilePath: string, packageDirectory: string, packageName: string, exports: unknown, conditions: string[], mode = MatchingMode.Exact): { moduleFileToTry: string } | undefined { 665 if (typeof exports === "string") { 666 const pathOrPattern = getNormalizedAbsolutePath(combinePaths(packageDirectory, exports), /*currentDirectory*/ undefined); 667 const extensionSwappedTarget = hasTSFileExtension(targetFilePath) ? removeFileExtension(targetFilePath) + tryGetJSExtensionForFile(targetFilePath, options) : undefined; 668 switch (mode) { 669 case MatchingMode.Exact: 670 if (comparePaths(targetFilePath, pathOrPattern) === Comparison.EqualTo || (extensionSwappedTarget && comparePaths(extensionSwappedTarget, pathOrPattern) === Comparison.EqualTo)) { 671 return { moduleFileToTry: packageName }; 672 } 673 break; 674 case MatchingMode.Directory: 675 if (containsPath(pathOrPattern, targetFilePath)) { 676 const fragment = getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false); 677 return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) }; 678 } 679 break; 680 case MatchingMode.Pattern: 681 const starPos = pathOrPattern.indexOf("*"); 682 const leadingSlice = pathOrPattern.slice(0, starPos); 683 const trailingSlice = pathOrPattern.slice(starPos + 1); 684 if (startsWith(targetFilePath, leadingSlice) && endsWith(targetFilePath, trailingSlice)) { 685 const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length); 686 return { moduleFileToTry: packageName.replace("*", starReplacement) }; 687 } 688 if (extensionSwappedTarget && startsWith(extensionSwappedTarget, leadingSlice) && endsWith(extensionSwappedTarget, trailingSlice)) { 689 const starReplacement = extensionSwappedTarget.slice(leadingSlice.length, extensionSwappedTarget.length - trailingSlice.length); 690 return { moduleFileToTry: packageName.replace("*", starReplacement) }; 691 } 692 break; 693 } 694 } 695 else if (Array.isArray(exports)) { 696 return forEach(exports, e => tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, e, conditions)); 697 } 698 else if (typeof exports === "object" && exports !== null) { // eslint-disable-line no-null/no-null 699 if (allKeysStartWithDot(exports as MapLike<unknown>)) { 700 // sub-mappings 701 // 3 cases: 702 // * directory mappings (legacyish, key ends with / (technically allows index/extension resolution under cjs mode)) 703 // * pattern mappings (contains a *) 704 // * exact mappings (no *, does not end with /) 705 return forEach(getOwnKeys(exports as MapLike<unknown>), k => { 706 const subPackageName = getNormalizedAbsolutePath(combinePaths(packageName, k), /*currentDirectory*/ undefined); 707 const mode = endsWith(k, "/") ? MatchingMode.Directory 708 : stringContains(k, "*") ? MatchingMode.Pattern 709 : MatchingMode.Exact; 710 return tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, subPackageName, (exports as MapLike<unknown>)[k], conditions, mode); 711 }); 712 } 713 else { 714 // conditional mapping 715 for (const key of getOwnKeys(exports as MapLike<unknown>)) { 716 if (key === "default" || conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(conditions, key)) { 717 const subTarget = (exports as MapLike<unknown>)[key]; 718 const result = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions); 719 if (result) { 720 return result; 721 } 722 } 723 } 724 } 725 } 726 return undefined; 727 } 728 729 function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined { 730 const normalizedTargetPaths = getPathsRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); 731 if (normalizedTargetPaths === undefined) { 732 return undefined; 733 } 734 735 const normalizedSourcePaths = getPathsRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); 736 const relativePaths = flatMap(normalizedSourcePaths, sourcePath => { 737 return map(normalizedTargetPaths, targetPath => ensurePathIsNonModuleName(getRelativePathFromDirectory(sourcePath, targetPath, getCanonicalFileName))); 738 }); 739 const shortest = min(relativePaths, compareNumberOfDirectorySeparators); 740 if (!shortest) { 741 return undefined; 742 } 743 744 return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs 745 ? removeExtensionAndIndexPostFix(shortest, ending, compilerOptions) 746 : removeFileExtension(shortest); 747 } 748 749 function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile , host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ModuleKind.ESNext | ModuleKind.CommonJS): string | undefined { 750 if (!host.fileExists || !host.readFile) { 751 return undefined; 752 } 753 const parts: NodeModulePathParts = getNodeModulePathParts(path, isOhpm(options.packageManagerType) ? ohModulesPathPart : nodeModulesPathPart)!; 754 if (!parts) { 755 return undefined; 756 } 757 758 // Simplify the full file path to something that can be resolved by Node. 759 760 const preferences = getPreferences(host, userPreferences, options, importingSourceFile); 761 let moduleSpecifier = path; 762 let isPackageRootPath = false; 763 if (!packageNameOnly) { 764 let packageRootIndex = parts.packageRootIndex; 765 let moduleFileName: string | undefined; 766 while (true) { 767 // If the module could be imported by a directory name, use that directory's name 768 const { moduleFileToTry, packageRootPath, blockedByExports, verbatimFromExports } = tryDirectoryWithPackageJson(packageRootIndex); 769 if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Classic) { 770 if (blockedByExports) { 771 return undefined; // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution 772 } 773 if (verbatimFromExports) { 774 return moduleFileToTry; 775 } 776 } 777 if (packageRootPath) { 778 moduleSpecifier = packageRootPath; 779 isPackageRootPath = true; 780 break; 781 } 782 if (!moduleFileName) moduleFileName = moduleFileToTry; 783 784 // try with next level of directory 785 packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1); 786 if (packageRootIndex === -1) { 787 moduleSpecifier = removeExtensionAndIndexPostFix(moduleFileName, preferences.ending, options, host); 788 break; 789 } 790 } 791 } 792 793 if (isRedirect && !isPackageRootPath) { 794 return undefined; 795 } 796 797 const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation(); 798 // Get a path that's relative to node_modules, oh_modules or the importing file's path 799 // if node_modules or oh_modules folder is in this folder or any of its parent folders, no need to keep it. 800 const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)); 801 if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) { 802 return undefined; 803 } 804 805 // If the module was found in @types, get the actual Node package name 806 const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); 807 const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName); 808 // For classic resolution, only allow importing from node_modules/@types, not other node_modules 809 return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Classic && packageName === nodeModulesDirectoryName ? undefined : packageName; 810 811 function tryDirectoryWithPackageJson(packageRootIndex: number): { moduleFileToTry: string, packageRootPath?: string, blockedByExports?: true, verbatimFromExports?: true } { 812 const packageRootPath = path.substring(0, packageRootIndex); 813 const packageJsonPath = combinePaths(packageRootPath, getPackageJsonByPMType(options.packageManagerType)); 814 let moduleFileToTry = path; 815 let maybeBlockedByTypesVersions = false; 816 const cachedPackageJson = host.getPackageJsonInfoCache?.()?.getPackageJsonInfo(packageJsonPath); 817 if (typeof cachedPackageJson === "object" || cachedPackageJson === undefined && host.fileExists(packageJsonPath)) { 818 const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || 819 isOhpm(options.packageManagerType) ? require("json5").parse(host.readFile!(packageJsonPath)!) : JSON.parse(host.readFile!(packageJsonPath)!); 820 const importMode = overrideMode || importingSourceFile.impliedNodeFormat; 821 if (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node16 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext) { 822 const conditions = ["node", importMode === ModuleKind.ESNext ? "import" : "require", "types"]; 823 const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" 824 ? tryGetModuleNameFromExports(options, path, packageRootPath, getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, conditions) 825 : undefined; 826 if (fromExports) { 827 const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) 828 ? fromExports 829 : { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) }; 830 return { ...withJsExtension, verbatimFromExports: true }; 831 } 832 if (packageJsonContent.exports) { 833 return { moduleFileToTry: path, blockedByExports: true }; 834 } 835 } 836 const versionPaths = packageJsonContent.typesVersions 837 ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) 838 : undefined; 839 if (versionPaths) { 840 const subModuleName = path.slice(packageRootPath.length + 1); 841 const fromPaths = tryGetModuleNameFromPaths( 842 subModuleName, 843 versionPaths.paths, 844 getAllowedEndings(preferences.ending, options, importMode), 845 host, 846 options 847 ); 848 if (fromPaths === undefined) { 849 maybeBlockedByTypesVersions = true; 850 } 851 else { 852 moduleFileToTry = combinePaths(packageRootPath, fromPaths); 853 } 854 } 855 // If the file is the main module, it can be imported by the package name 856 const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main || "index.js"; 857 if (isString(mainFileRelative) && !(maybeBlockedByTypesVersions && matchPatternOrExact(tryParsePatterns(versionPaths!.paths), mainFileRelative))) { 858 // The 'main' file is also subject to mapping through typesVersions, and we couldn't come up with a path 859 // explicitly through typesVersions, so if it matches a key in typesVersions now, it's not reachable. 860 // (The only way this can happen is if some file in a package that's not resolvable from outside the 861 // package got pulled into the program anyway, e.g. transitively through a file that *is* reachable. It 862 // happens very easily in fourslash tests though, since every test file listed gets included. See 863 // importNameCodeFix_typesVersions.ts for an example.) 864 const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); 865 if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) { 866 // ^ An arbitrary removal of file extension for this comparison is almost certainly wrong 867 return { packageRootPath, moduleFileToTry }; 868 } 869 } 870 } 871 else { 872 // No package.json exists; an index.js will still resolve as the package name 873 const fileName = getCanonicalFileName(moduleFileToTry.substring(parts.packageRootIndex + 1)); 874 if (fileName === "index.d.ts" || fileName === "index.js" || fileName === "index.ts" || fileName === "index.tsx") { 875 return { moduleFileToTry, packageRootPath }; 876 } 877 } 878 return { moduleFileToTry }; 879 } 880 } 881 882 function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) { 883 if (!host.fileExists) return; 884 // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory 885 const extensions = flatten(getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }])); 886 for (const e of extensions) { 887 const fullPath = path + e; 888 if (host.fileExists(fullPath)) { 889 return fullPath; 890 } 891 } 892 } 893 894 function getPathsRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string[] | undefined { 895 return mapDefined(rootDirs, rootDir => { 896 const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName); 897 return relativePath !== undefined && isPathRelativeToParent(relativePath) ? undefined : relativePath; 898 }); 899 } 900 901 function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions, host?: ModuleSpecifierResolutionHost): string { 902 if (fileExtensionIsOneOf(fileName, [Extension.Json, Extension.Mjs, Extension.Cjs])) return fileName; 903 const noExtension = removeFileExtension(fileName); 904 if (fileName === noExtension) return fileName; 905 if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) return noExtension + getJSExtensionForFile(fileName, options); 906 switch (ending) { 907 case Ending.Minimal: 908 const withoutIndex = removeSuffix(noExtension, "/index"); 909 if (host && withoutIndex !== noExtension && tryGetAnyFileFromPath(host, withoutIndex)) { 910 // Can't remove index if there's a file by the same name as the directory. 911 // Probably more callers should pass `host` so we can determine this? 912 return noExtension; 913 } 914 return withoutIndex; 915 case Ending.Index: 916 return noExtension; 917 case Ending.JsExtension: 918 return noExtension + getJSExtensionForFile(fileName, options); 919 default: 920 return Debug.assertNever(ending); 921 } 922 } 923 924 function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension { 925 return tryGetJSExtensionForFile(fileName, options) ?? Debug.fail(`Extension ${extensionFromPath(fileName)} is unsupported:: FileName:: ${fileName}`); 926 } 927 928 export function tryGetJSExtensionForFile(fileName: string, options: CompilerOptions): Extension | undefined { 929 const ext = tryGetExtensionFromPath(fileName); 930 switch (ext) { 931 case Extension.Ts: 932 case Extension.Dts: 933 case Extension.Ets: 934 case Extension.Dets: 935 return Extension.Js; 936 case Extension.Tsx: 937 return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js; 938 case Extension.Js: 939 case Extension.Jsx: 940 case Extension.Json: 941 return ext; 942 case Extension.Dmts: 943 case Extension.Mts: 944 case Extension.Mjs: 945 return Extension.Mjs; 946 case Extension.Dcts: 947 case Extension.Cts: 948 case Extension.Cjs: 949 return Extension.Cjs; 950 default: 951 return undefined; 952 } 953 } 954 955 function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { 956 const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); 957 return isRootedDiskPath(relativePath) ? undefined : relativePath; 958 } 959 960 function isPathRelativeToParent(path: string): boolean { 961 return startsWith(path, ".."); 962 } 963} 964