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({ 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) ? Ending.JsExtension 29 : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; 30 } 31 } 32 } 33 34 function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { 35 return { 36 relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, 37 ending: hasJSFileExtension(oldImportSpecifier) ? 38 Ending.JsExtension : 39 getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, 40 }; 41 } 42 43 export function updateModuleSpecifier( 44 compilerOptions: CompilerOptions, 45 importingSourceFileName: Path, 46 toFileName: string, 47 host: ModuleSpecifierResolutionHost, 48 oldImportSpecifier: string, 49 ): string | undefined { 50 const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier)); 51 if (res === oldImportSpecifier) return undefined; 52 return res; 53 } 54 55 // Note: importingSourceFile is just for usesJsExtensionOnImports 56 export function getModuleSpecifier( 57 compilerOptions: CompilerOptions, 58 importingSourceFile: SourceFile, 59 importingSourceFileName: Path, 60 toFileName: string, 61 host: ModuleSpecifierResolutionHost, 62 preferences: UserPreferences = {}, 63 ): string { 64 return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferences(preferences, compilerOptions, importingSourceFile)); 65 } 66 67 export function getNodeModulesPackageName( 68 compilerOptions: CompilerOptions, 69 importingSourceFileName: Path, 70 nodeModulesFileName: string, 71 host: ModuleSpecifierResolutionHost, 72 ): string | undefined { 73 const info = getInfo(importingSourceFileName, host); 74 const modulePaths = getAllModulePaths(importingSourceFileName, nodeModulesFileName, host); 75 return firstDefined(modulePaths, 76 modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions, /*packageNameOnly*/ true)); 77 } 78 79 function getModuleSpecifierWorker( 80 compilerOptions: CompilerOptions, 81 importingSourceFileName: Path, 82 toFileName: string, 83 host: ModuleSpecifierResolutionHost, 84 preferences: Preferences 85 ): string { 86 const info = getInfo(importingSourceFileName, host); 87 const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host); 88 return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions)) || 89 getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences); 90 } 91 92 /** Returns an import for each symlink and for the realpath. */ 93 export function getModuleSpecifiers( 94 moduleSymbol: Symbol, 95 checker: TypeChecker, 96 compilerOptions: CompilerOptions, 97 importingSourceFile: SourceFile, 98 host: ModuleSpecifierResolutionHost, 99 userPreferences: UserPreferences, 100 ): readonly string[] { 101 const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker); 102 if (ambient) return [ambient]; 103 104 const info = getInfo(importingSourceFile.path, host); 105 const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol)); 106 const modulePaths = getAllModulePaths(importingSourceFile.path, moduleSourceFile.originalFileName, host); 107 const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); 108 109 const existingSpecifier = forEach(modulePaths, modulePath => forEach( 110 host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), 111 reason => { 112 if (reason.kind !== FileIncludeKind.Import || reason.file !== importingSourceFile.path) return undefined; 113 const specifier = getModuleNameStringLiteralAt(importingSourceFile, reason.index).text; 114 // If the preference is for non relative and the module specifier is relative, ignore it 115 return preferences.relativePreference !== RelativePreference.NonRelative || !pathIsRelative(specifier) ? 116 specifier : 117 undefined; 118 } 119 )); 120 if (existingSpecifier) return [existingSpecifier]; 121 122 const importedFileIsInNodeModules = some(modulePaths, p => p.isInNodeModules); 123 124 // Module specifier priority: 125 // 1. "Bare package specifiers" (e.g. "@foo/bar") resulting from a path through node_modules to a package.json's "types" entry 126 // 2. Specifiers generated using "paths" from tsconfig 127 // 3. Non-relative specfiers resulting from a path through node_modules (e.g. "@foo/bar/path/to/file") 128 // 4. Relative paths 129 let nodeModulesSpecifiers: string[] | undefined; 130 let pathsSpecifiers: string[] | undefined; 131 let relativeSpecifiers: string[] | undefined; 132 for (const modulePath of modulePaths) { 133 const specifier = tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions); 134 nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier); 135 if (specifier && modulePath.isRedirect) { 136 // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", 137 // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking. 138 return nodeModulesSpecifiers!; 139 } 140 141 if (!specifier && !modulePath.isRedirect) { 142 const local = getLocalModuleSpecifier(modulePath.path, info, compilerOptions, host, preferences); 143 if (pathIsBareSpecifier(local)) { 144 pathsSpecifiers = append(pathsSpecifiers, local); 145 } 146 else if (!importedFileIsInNodeModules || modulePath.isInNodeModules) { 147 // Why this extra conditional, not just an `else`? If some path to the file contained 148 // 'node_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"), 149 // that means we had to go through a *sibling's* node_modules, not one we can access directly. 150 // If some path to the file was in node_modules but another was not, this likely indicates that 151 // we have a monorepo structure with symlinks. In this case, the non-node_modules path is 152 // probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package 153 // in a monorepo is probably not portable. So, the module specifier we actually go with will be 154 // the relative path through node_modules, so that the declaration emitter can produce a 155 // portability error. (See declarationEmitReexportedSymlinkReference3) 156 relativeSpecifiers = append(relativeSpecifiers, local); 157 } 158 } 159 } 160 161 return pathsSpecifiers?.length ? pathsSpecifiers : 162 nodeModulesSpecifiers?.length ? nodeModulesSpecifiers : 163 Debug.checkDefined(relativeSpecifiers); 164 } 165 166 interface Info { 167 readonly getCanonicalFileName: GetCanonicalFileName; 168 readonly importingSourceFileName: Path 169 readonly sourceDirectory: Path; 170 } 171 // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path 172 function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info { 173 const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); 174 const sourceDirectory = getDirectoryPath(importingSourceFileName); 175 return { getCanonicalFileName, importingSourceFileName, sourceDirectory }; 176 } 177 178 function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, { ending, relativePreference }: Preferences): string { 179 const { baseUrl, paths, rootDirs } = compilerOptions; 180 const { sourceDirectory, getCanonicalFileName } = info; 181 182 const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || 183 removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); 184 if (!baseUrl && !paths || relativePreference === RelativePreference.Relative) { 185 return relativePath; 186 } 187 188 const baseDirectory = getPathsBasePath(compilerOptions, host) || baseUrl!; 189 const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseDirectory, getCanonicalFileName); 190 if (!relativeToBaseUrl) { 191 return relativePath; 192 } 193 194 const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions); 195 const fromPaths = paths && tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); 196 const nonRelative = fromPaths === undefined && baseUrl !== undefined ? importRelativeToBaseUrl : fromPaths; 197 if (!nonRelative) { 198 return relativePath; 199 } 200 201 if (relativePreference === RelativePreference.NonRelative) { 202 return nonRelative; 203 } 204 205 if (relativePreference === RelativePreference.ExternalNonRelative) { 206 const projectDirectory = host.getCurrentDirectory(); 207 const modulePath = toPath(moduleFileName, projectDirectory, getCanonicalFileName); 208 const sourceIsInternal = startsWith(sourceDirectory, projectDirectory); 209 const targetIsInternal = startsWith(modulePath, projectDirectory); 210 if (sourceIsInternal && !targetIsInternal || !sourceIsInternal && targetIsInternal) { 211 // 1. The import path crosses the boundary of the tsconfig.json-containing directory. 212 // 213 // src/ 214 // tsconfig.json 215 // index.ts ------- 216 // lib/ | (path crosses tsconfig.json) 217 // imported.ts <--- 218 // 219 return nonRelative; 220 } 221 222 const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, getDirectoryPath(modulePath)); 223 const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory); 224 if (nearestSourcePackageJson !== nearestTargetPackageJson) { 225 // 2. The importing and imported files are part of different packages. 226 // 227 // packages/a/ 228 // package.json 229 // index.ts -------- 230 // packages/b/ | (path crosses package.json) 231 // package.json | 232 // component.ts <--- 233 // 234 return nonRelative; 235 } 236 237 return relativePath; 238 } 239 240 if (relativePreference !== RelativePreference.Shortest) Debug.assertNever(relativePreference); 241 242 // Prefer a relative import over a baseUrl import if it has fewer components. 243 return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative; 244 } 245 246 export function countPathComponents(path: string): number { 247 let count = 0; 248 for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) { 249 if (path.charCodeAt(i) === CharacterCodes.slash) count++; 250 } 251 return count; 252 } 253 254 function usesJsExtensionOnImports({ imports }: SourceFile): boolean { 255 return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false; 256 } 257 258 function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) { 259 return compareBooleans(b.isRedirect, a.isRedirect) || compareNumberOfDirectorySeparators(a.path, b.path); 260 } 261 262 function getNearestAncestorDirectoryWithPackageJson(host: ModuleSpecifierResolutionHost, fileName: string) { 263 if (host.getNearestAncestorDirectoryWithPackageJson) { 264 return host.getNearestAncestorDirectoryWithPackageJson(fileName); 265 } 266 return !!forEachAncestorDirectory(fileName, directory => { 267 return host.fileExists(combinePaths(directory, "package.json")) ? true : undefined; 268 }); 269 } 270 271 export function forEachFileNameOfModule<T>( 272 importingFileName: string, 273 importedFileName: string, 274 host: ModuleSpecifierResolutionHost, 275 preferSymlinks: boolean, 276 cb: (fileName: string, isRedirect: boolean) => T | undefined 277 ): T | undefined { 278 const getCanonicalFileName = hostGetCanonicalFileName(host); 279 const cwd = host.getCurrentDirectory(); 280 const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined; 281 const importedPath = toPath(importedFileName, cwd, getCanonicalFileName); 282 const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray; 283 const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects]; 284 const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd)); 285 let shouldFilterIgnoredPaths = !every(targets, containsIgnoredPath); 286 287 if (!preferSymlinks) { 288 // Symlinks inside ignored paths are already filtered out of the symlink cache, 289 // so we only need to remove them from the realpath filenames. 290 const result = forEach(targets, p => !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) && cb(p, referenceRedirect === p)); 291 if (result) return result; 292 } 293 const links = host.getSymlinkCache 294 ? host.getSymlinkCache() 295 : discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd); 296 297 const symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath(); 298 const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd); 299 const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => { 300 const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName))); 301 if (!symlinkDirectories) return undefined; // Continue to ancestor directory 302 303 // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) 304 if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { 305 return false; // Stop search, each ancestor directory will also hit this condition 306 } 307 308 return forEach(targets, target => { 309 if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { 310 return; 311 } 312 313 const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); 314 for (const symlinkDirectory of symlinkDirectories) { 315 const option = resolvePath(symlinkDirectory, relative); 316 const result = cb(option, target === referenceRedirect); 317 shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths 318 if (result) return result; 319 } 320 }); 321 }); 322 return result || (preferSymlinks 323 ? forEach(targets, p => shouldFilterIgnoredPaths && containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect)) 324 : undefined); 325 } 326 327 interface ModulePath { 328 path: string; 329 isInNodeModules: boolean; 330 isRedirect: boolean; 331 } 332 333 /** 334 * Looks for existing imports that use symlinks to this module. 335 * Symlinks will be returned first so they are preferred over the real path. 336 */ 337 function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly ModulePath[] { 338 const cwd = host.getCurrentDirectory(); 339 const getCanonicalFileName = hostGetCanonicalFileName(host); 340 const allFileNames = new Map<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>(); 341 let importedFileFromNodeModules = false; 342 forEachFileNameOfModule( 343 importingFileName, 344 importedFileName, 345 host, 346 /*preferSymlinks*/ true, 347 (path, isRedirect) => { 348 const isInNodeModules = pathContainsNodeModules(path); 349 allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules }); 350 importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules; 351 // don't return value, so we collect everything 352 } 353 ); 354 355 // Sort by paths closest to importing file Name directory 356 const sortedPaths: ModulePath[] = []; 357 for ( 358 let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName)); 359 allFileNames.size !== 0; 360 ) { 361 const directoryStart = ensureTrailingDirectorySeparator(directory); 362 let pathsInDirectory: ModulePath[] | undefined; 363 allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => { 364 if (startsWith(path, directoryStart)) { 365 (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules }); 366 allFileNames.delete(fileName); 367 } 368 }); 369 if (pathsInDirectory) { 370 if (pathsInDirectory.length > 1) { 371 pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); 372 } 373 sortedPaths.push(...pathsInDirectory); 374 } 375 const newDirectory = getDirectoryPath(directory); 376 if (newDirectory === directory) break; 377 directory = newDirectory; 378 } 379 if (allFileNames.size) { 380 const remainingPaths = arrayFrom(allFileNames.values()); 381 if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); 382 sortedPaths.push(...remainingPaths); 383 } 384 return sortedPaths; 385 } 386 387 function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeChecker): string | undefined { 388 const decl = find(moduleSymbol.declarations, 389 d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name))) 390 ) as (ModuleDeclaration & { name: StringLiteral }) | undefined; 391 if (decl) { 392 return decl.name.text; 393 } 394 395 // the module could be a namespace, which is export through "export=" from an ambient module. 396 /** 397 * declare module "m" { 398 * namespace ns { 399 * class c {} 400 * } 401 * export = ns; 402 * } 403 */ 404 // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m" 405 const ambientModuleDeclareCandidates = mapDefined(moduleSymbol.declarations, 406 d => { 407 if (!isModuleDeclaration(d)) return; 408 const topNamespace = getTopNamespace(d); 409 if (!(topNamespace?.parent?.parent 410 && isModuleBlock(topNamespace.parent) && isAmbientModule(topNamespace.parent.parent) && isSourceFile(topNamespace.parent.parent.parent))) return; 411 const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as __String)?.valueDeclaration as ExportAssignment)?.expression as PropertyAccessExpression | Identifier); 412 if (!exportAssignment) return; 413 const exportSymbol = checker.getSymbolAtLocation(exportAssignment); 414 if (!exportSymbol) return; 415 const originalExportSymbol = exportSymbol?.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol; 416 if (originalExportSymbol === d.symbol) return topNamespace.parent.parent; 417 418 function getTopNamespace(namespaceDeclaration: ModuleDeclaration) { 419 while (namespaceDeclaration.flags & NodeFlags.NestedNamespace) { 420 namespaceDeclaration = namespaceDeclaration.parent as ModuleDeclaration; 421 } 422 return namespaceDeclaration; 423 } 424 } 425 ); 426 const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (AmbientModuleDeclaration & { name: StringLiteral }) | undefined; 427 if (ambientModuleDeclare) { 428 return ambientModuleDeclare.name.text; 429 } 430 } 431 432 function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike<readonly string[]>): string | undefined { 433 for (const key in paths) { 434 for (const patternText of paths[key]) { 435 const pattern = removeFileExtension(normalizePath(patternText)); 436 const indexOfStar = pattern.indexOf("*"); 437 if (indexOfStar !== -1) { 438 const prefix = pattern.substr(0, indexOfStar); 439 const suffix = pattern.substr(indexOfStar + 1); 440 if (relativeToBaseUrl.length >= prefix.length + suffix.length && 441 startsWith(relativeToBaseUrl, prefix) && 442 endsWith(relativeToBaseUrl, suffix) || 443 !suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) { 444 const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length); 445 return key.replace("*", matchedStar); 446 } 447 } 448 else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) { 449 return key; 450 } 451 } 452 } 453 } 454 455 function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined { 456 const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); 457 if (normalizedTargetPath === undefined) { 458 return undefined; 459 } 460 461 const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); 462 const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; 463 return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs 464 ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions) 465 : removeFileExtension(relativePath); 466 } 467 468 function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { 469 if (!host.fileExists || !host.readFile) { 470 return undefined; 471 } 472 const parts: NodeModulePathParts = getNodeModulePathParts(path)!; 473 if (!parts) { 474 return undefined; 475 } 476 477 // Simplify the full file path to something that can be resolved by Node. 478 479 let moduleSpecifier = path; 480 let isPackageRootPath = false; 481 if (!packageNameOnly) { 482 let packageRootIndex = parts.packageRootIndex; 483 let moduleFileNameForExtensionless: string | undefined; 484 while (true) { 485 // If the module could be imported by a directory name, use that directory's name 486 const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex); 487 if (packageRootPath) { 488 moduleSpecifier = packageRootPath; 489 isPackageRootPath = true; 490 break; 491 } 492 if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry; 493 494 // try with next level of directory 495 packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1); 496 if (packageRootIndex === -1) { 497 moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless); 498 break; 499 } 500 } 501 } 502 503 if (isRedirect && !isPackageRootPath) { 504 return undefined; 505 } 506 507 const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation(); 508 // Get a path that's relative to node_modules or the importing file's path 509 // if node_modules folder is in this folder or any of its parent folders, no need to keep it. 510 const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)); 511 if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) { 512 return undefined; 513 } 514 515 // If the module was found in @types, get the actual Node package name 516 const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); 517 const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName); 518 // For classic resolution, only allow importing from node_modules/@types, not other node_modules 519 return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName; 520 521 function tryDirectoryWithPackageJson(packageRootIndex: number) { 522 const packageRootPath = path.substring(0, packageRootIndex); 523 const packageJsonPath = combinePaths(packageRootPath, "package.json"); 524 let moduleFileToTry = path; 525 if (host.fileExists(packageJsonPath)) { 526 const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!); 527 const versionPaths = packageJsonContent.typesVersions 528 ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) 529 : undefined; 530 if (versionPaths) { 531 const subModuleName = path.slice(packageRootPath.length + 1); 532 const fromPaths = tryGetModuleNameFromPaths( 533 removeFileExtension(subModuleName), 534 removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), 535 versionPaths.paths 536 ); 537 if (fromPaths !== undefined) { 538 moduleFileToTry = combinePaths(packageRootPath, fromPaths); 539 } 540 } 541 542 // If the file is the main module, it can be imported by the package name 543 const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; 544 if (isString(mainFileRelative)) { 545 const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); 546 if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) { 547 return { packageRootPath, moduleFileToTry }; 548 } 549 } 550 } 551 return { moduleFileToTry }; 552 } 553 554 function getExtensionlessFileName(path: string): string { 555 // We still have a file name - remove the extension 556 const fullModulePathWithoutExtension = removeFileExtension(path); 557 558 // If the file is /index, it can be imported by its directory name 559 // IFF there is not _also_ a file by the same name 560 if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) { 561 return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex); 562 } 563 564 return fullModulePathWithoutExtension; 565 } 566 } 567 568 function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) { 569 if (!host.fileExists) return; 570 // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory 571 const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]); 572 for (const e of extensions) { 573 const fullPath = path + e; 574 if (host.fileExists(fullPath)) { 575 return fullPath; 576 } 577 } 578 } 579 580 interface NodeModulePathParts { 581 readonly topLevelNodeModulesIndex: number; 582 readonly topLevelPackageNameIndex: number; 583 readonly packageRootIndex: number; 584 readonly fileNameIndex: number; 585 } 586 function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined { 587 // If fullPath can't be valid module file within node_modules, returns undefined. 588 // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js 589 // Returns indices: ^ ^ ^ ^ 590 591 let topLevelNodeModulesIndex = 0; 592 let topLevelPackageNameIndex = 0; 593 let packageRootIndex = 0; 594 let fileNameIndex = 0; 595 596 const enum States { 597 BeforeNodeModules, 598 NodeModules, 599 Scope, 600 PackageContent 601 } 602 603 let partStart = 0; 604 let partEnd = 0; 605 let state = States.BeforeNodeModules; 606 607 while (partEnd >= 0) { 608 partStart = partEnd; 609 partEnd = fullPath.indexOf("/", partStart + 1); 610 switch (state) { 611 case States.BeforeNodeModules: 612 if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) { 613 topLevelNodeModulesIndex = partStart; 614 topLevelPackageNameIndex = partEnd; 615 state = States.NodeModules; 616 } 617 break; 618 case States.NodeModules: 619 case States.Scope: 620 if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") { 621 state = States.Scope; 622 } 623 else { 624 packageRootIndex = partEnd; 625 state = States.PackageContent; 626 } 627 break; 628 case States.PackageContent: 629 if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) { 630 state = States.NodeModules; 631 } 632 else { 633 state = States.PackageContent; 634 } 635 break; 636 } 637 } 638 639 fileNameIndex = partStart; 640 641 return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined; 642 } 643 644 function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string | undefined { 645 return firstDefined(rootDirs, rootDir => { 646 const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)!; // TODO: GH#18217 647 return isPathRelativeToParent(relativePath) ? undefined : relativePath; 648 }); 649 } 650 651 function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string { 652 if (fileExtensionIs(fileName, Extension.Json)) return fileName; 653 const noExtension = removeFileExtension(fileName); 654 switch (ending) { 655 case Ending.Minimal: 656 return removeSuffix(noExtension, "/index"); 657 case Ending.Index: 658 return noExtension; 659 case Ending.JsExtension: 660 return noExtension + getJSExtensionForFile(fileName, options); 661 default: 662 return Debug.assertNever(ending); 663 } 664 } 665 666 function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension { 667 const ext = extensionFromPath(fileName); 668 switch (ext) { 669 case Extension.Ts: 670 case Extension.Dts: 671 case Extension.Ets: 672 return Extension.Js; 673 case Extension.Tsx: 674 return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js; 675 case Extension.Js: 676 case Extension.Jsx: 677 case Extension.Json: 678 return ext; 679 case Extension.TsBuildInfo: 680 return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported:: FileName:: ${fileName}`); 681 default: 682 return Debug.assertNever(ext); 683 } 684 } 685 686 function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { 687 const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); 688 return isRootedDiskPath(relativePath) ? undefined : relativePath; 689 } 690 691 function isPathRelativeToParent(path: string): boolean { 692 return startsWith(path, ".."); 693 } 694} 695