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 getModulesPackageName( 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, compilerOptions.packageManagerType); 75 return firstDefined(modulePaths, 76 modulePath => tryGetModuleNameAsExternalModule(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,compilerOptions.packageManagerType); 88 return firstDefined(modulePaths, modulePath => tryGetModuleNameAsExternalModule(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, compilerOptions.packageManagerType); 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 or oh_modules to a package.json's or oh-package.json5's "types" entry 126 // 2. Specifiers generated using "paths" from tsconfig 127 // 3. Non-relative specfiers resulting from a path through node_modules or oh_modules(e.g. "@foo/bar/path/to/file") 128 // 4. Relative paths 129 let modulesSpecifiers: string[] | undefined; 130 let pathsSpecifiers: string[] | undefined; 131 let relativeSpecifiers: string[] | undefined; 132 for (const modulePath of modulePaths) { 133 const specifier = tryGetModuleNameAsExternalModule(modulePath, info, host, compilerOptions); 134 modulesSpecifiers = append(modulesSpecifiers, 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 modulesSpecifiers!; 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' or 'oh_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 or oh_modules, not one we can access directly. 150 // If some path to the file was in node_modules or oh_modules but another was not, this likely indicates that 151 // we have a monorepo structure with symlinks. In this case, the non-node_modules or oh_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 or oh_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 modulesSpecifiers?.length ? modulesSpecifiers : 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 const packageManagerType = compilerOptions.packageManagerType; 222 const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, getDirectoryPath(modulePath), packageManagerType); 223 const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory, packageManagerType); 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, packageManagerType?: string) { 263 if (host.getNearestAncestorDirectoryWithPackageJson) { 264 return host.getNearestAncestorDirectoryWithPackageJson(fileName); 265 } 266 return !!forEachAncestorDirectory(fileName, directory => { 267 return host.fileExists(combinePaths(directory, getPackageJsonByPMType(packageManagerType))) ? 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 isOHModules?: boolean 278 ): T | undefined { 279 const getCanonicalFileName = hostGetCanonicalFileName(host); 280 const cwd = host.getCurrentDirectory(); 281 const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined; 282 const importedPath = toPath(importedFileName, cwd, getCanonicalFileName); 283 const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray; 284 const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects]; 285 const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd)); 286 let shouldFilterIgnoredPaths = !every(targets, containsIgnoredPath); 287 288 if (!preferSymlinks) { 289 // Symlinks inside ignored paths are already filtered out of the symlink cache, 290 // so we only need to remove them from the realpath filenames. 291 const result = forEach(targets, p => !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) && cb(p, referenceRedirect === p)); 292 if (result) return result; 293 } 294 const links = host.getSymlinkCache 295 ? host.getSymlinkCache() 296 : discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd, isOHModules); 297 298 const symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath(); 299 const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd); 300 const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => { 301 const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName))); 302 if (!symlinkDirectories) return undefined; // Continue to ancestor directory 303 304 // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) 305 if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { 306 return false; // Stop search, each ancestor directory will also hit this condition 307 } 308 309 return forEach(targets, target => { 310 if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { 311 return; 312 } 313 314 const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); 315 for (const symlinkDirectory of symlinkDirectories) { 316 const option = resolvePath(symlinkDirectory, relative); 317 const result = cb(option, target === referenceRedirect); 318 shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths 319 if (result) return result; 320 } 321 }); 322 }); 323 return result || (preferSymlinks 324 ? forEach(targets, p => shouldFilterIgnoredPaths && containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect)) 325 : undefined); 326 } 327 328 interface ModulePath { 329 path: string; 330 isInNodeModules: boolean; 331 isRedirect: boolean; 332 } 333 334 /** 335 * Looks for existing imports that use symlinks to this module. 336 * Symlinks will be returned first so they are preferred over the real path. 337 */ 338 function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost, packageManagerType?: string): readonly ModulePath[] { 339 const cwd = host.getCurrentDirectory(); 340 const getCanonicalFileName = hostGetCanonicalFileName(host); 341 const allFileNames = new Map<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>(); 342 let importedFileFromNodeModules = false; 343 const isOHModules: boolean =isOhpm(packageManagerType); 344 forEachFileNameOfModule( 345 importingFileName, 346 importedFileName, 347 host, 348 /*preferSymlinks*/ true, 349 (path, isRedirect) => { 350 const isInNodeModules = isOhpm(packageManagerType) ? pathContainsOHModules(path) : pathContainsNodeModules(path); 351 allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules }); 352 importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules; 353 // don't return value, so we collect everything 354 }, 355 isOHModules 356 ); 357 358 // Sort by paths closest to importing file Name directory 359 const sortedPaths: ModulePath[] = []; 360 for ( 361 let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName)); 362 allFileNames.size !== 0; 363 ) { 364 const directoryStart = ensureTrailingDirectorySeparator(directory); 365 let pathsInDirectory: ModulePath[] | undefined; 366 allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => { 367 if (startsWith(path, directoryStart)) { 368 (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules }); 369 allFileNames.delete(fileName); 370 } 371 }); 372 if (pathsInDirectory) { 373 if (pathsInDirectory.length > 1) { 374 pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); 375 } 376 sortedPaths.push(...pathsInDirectory); 377 } 378 const newDirectory = getDirectoryPath(directory); 379 if (newDirectory === directory) break; 380 directory = newDirectory; 381 } 382 if (allFileNames.size) { 383 const remainingPaths = arrayFrom(allFileNames.values()); 384 if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); 385 sortedPaths.push(...remainingPaths); 386 } 387 return sortedPaths; 388 } 389 390 function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeChecker): string | undefined { 391 const decl = find(moduleSymbol.declarations, 392 d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name))) 393 ) as (ModuleDeclaration & { name: StringLiteral }) | undefined; 394 if (decl) { 395 return decl.name.text; 396 } 397 398 // the module could be a namespace, which is export through "export=" from an ambient module. 399 /** 400 * declare module "m" { 401 * namespace ns { 402 * class c {} 403 * } 404 * export = ns; 405 * } 406 */ 407 // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m" 408 const ambientModuleDeclareCandidates = mapDefined(moduleSymbol.declarations, 409 d => { 410 if (!isModuleDeclaration(d)) return; 411 const topNamespace = getTopNamespace(d); 412 if (!(topNamespace?.parent?.parent 413 && isModuleBlock(topNamespace.parent) && isAmbientModule(topNamespace.parent.parent) && isSourceFile(topNamespace.parent.parent.parent))) return; 414 const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as __String)?.valueDeclaration as ExportAssignment)?.expression as PropertyAccessExpression | Identifier); 415 if (!exportAssignment) return; 416 const exportSymbol = checker.getSymbolAtLocation(exportAssignment); 417 if (!exportSymbol) return; 418 const originalExportSymbol = exportSymbol?.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol; 419 if (originalExportSymbol === d.symbol) return topNamespace.parent.parent; 420 421 function getTopNamespace(namespaceDeclaration: ModuleDeclaration) { 422 while (namespaceDeclaration.flags & NodeFlags.NestedNamespace) { 423 namespaceDeclaration = namespaceDeclaration.parent as ModuleDeclaration; 424 } 425 return namespaceDeclaration; 426 } 427 } 428 ); 429 const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (AmbientModuleDeclaration & { name: StringLiteral }) | undefined; 430 if (ambientModuleDeclare) { 431 return ambientModuleDeclare.name.text; 432 } 433 } 434 435 function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike<readonly string[]>): string | undefined { 436 for (const key in paths) { 437 for (const patternText of paths[key]) { 438 const pattern = removeFileExtension(normalizePath(patternText)); 439 const indexOfStar = pattern.indexOf("*"); 440 if (indexOfStar !== -1) { 441 const prefix = pattern.substr(0, indexOfStar); 442 const suffix = pattern.substr(indexOfStar + 1); 443 if (relativeToBaseUrl.length >= prefix.length + suffix.length && 444 startsWith(relativeToBaseUrl, prefix) && 445 endsWith(relativeToBaseUrl, suffix) || 446 !suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) { 447 const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length); 448 return key.replace("*", matchedStar); 449 } 450 } 451 else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) { 452 return key; 453 } 454 } 455 } 456 } 457 458 function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined { 459 const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); 460 if (normalizedTargetPath === undefined) { 461 return undefined; 462 } 463 464 const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); 465 const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; 466 return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs 467 ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions) 468 : removeFileExtension(relativePath); 469 } 470 471 function tryGetModuleNameAsExternalModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { 472 if (!host.fileExists || !host.readFile) { 473 return undefined; 474 } 475 const parts: ModulePathParts = getModulePathParts(path, isOhpm(options.packageManagerType) ? ohModulesPathPart : nodeModulesPathPart)!; 476 if (!parts) { 477 return undefined; 478 } 479 480 // Simplify the full file path to something that can be resolved by Node. 481 482 let moduleSpecifier = path; 483 let isPackageRootPath = false; 484 if (!packageNameOnly) { 485 let packageRootIndex = parts.packageRootIndex; 486 let moduleFileNameForExtensionless: string | undefined; 487 while (true) { 488 // If the module could be imported by a directory name, use that directory's name 489 const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex); 490 if (packageRootPath) { 491 moduleSpecifier = packageRootPath; 492 isPackageRootPath = true; 493 break; 494 } 495 if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry; 496 497 // try with next level of directory 498 packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1); 499 if (packageRootIndex === -1) { 500 moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless); 501 break; 502 } 503 } 504 } 505 506 if (isRedirect && !isPackageRootPath) { 507 return undefined; 508 } 509 510 const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation(); 511 // Get a path that's relative to node_modules, oh_modules or the importing file's path 512 // if node_modules or oh_modules folder is in this folder or any of its parent folders, no need to keep it. 513 const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)); 514 if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) { 515 return undefined; 516 } 517 518 // If the module was found in @types, get the actual Node package name 519 const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); 520 const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName); 521 // For classic resolution, only allow importing from or oh_modules/@types, not other node_modules or oh_modules 522 return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName; 523 524 function tryDirectoryWithPackageJson(packageRootIndex: number) { 525 const packageRootPath = path.substring(0, packageRootIndex); 526 const packageJsonPath = combinePaths(packageRootPath, getPackageJsonByPMType(options.packageManagerType)); 527 let moduleFileToTry = path; 528 if (host.fileExists(packageJsonPath)) { 529 const isOHModules: boolean = isOhpm(options.packageManagerType); 530 const packageJsonContent = isOHModules ? JSON5.parse(host.readFile!(packageJsonPath)!) : JSON.parse(host.readFile!(packageJsonPath)!); 531 const versionPaths = packageJsonContent.typesVersions 532 ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) 533 : undefined; 534 if (versionPaths) { 535 const subModuleName = path.slice(packageRootPath.length + 1); 536 const fromPaths = tryGetModuleNameFromPaths( 537 removeFileExtension(subModuleName), 538 removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), 539 versionPaths.paths 540 ); 541 if (fromPaths !== undefined) { 542 moduleFileToTry = combinePaths(packageRootPath, fromPaths); 543 } 544 } 545 546 // If the file is the main module, it can be imported by the package name 547 const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; 548 if (isString(mainFileRelative)) { 549 const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); 550 if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) { 551 return { packageRootPath, moduleFileToTry }; 552 } 553 } 554 } 555 return { moduleFileToTry }; 556 } 557 558 function getExtensionlessFileName(path: string): string { 559 // We still have a file name - remove the extension 560 const fullModulePathWithoutExtension = removeFileExtension(path); 561 562 // If the file is /index, it can be imported by its directory name 563 // IFF there is not _also_ a file by the same name 564 if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) { 565 return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex); 566 } 567 568 return fullModulePathWithoutExtension; 569 } 570 } 571 572 function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) { 573 if (!host.fileExists) return; 574 // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory 575 const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]); 576 for (const e of extensions) { 577 const fullPath = path + e; 578 if (host.fileExists(fullPath)) { 579 return fullPath; 580 } 581 } 582 } 583 584 interface ModulePathParts { 585 readonly topLevelNodeModulesIndex: number; 586 readonly topLevelPackageNameIndex: number; 587 readonly packageRootIndex: number; 588 readonly fileNameIndex: number; 589 } 590 function getModulePathParts(fullPath: string, modulesPathPart: string): ModulePathParts | undefined { 591 // If fullPath can't be valid module file within node_modules or oh_modules, returns undefined. 592 // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js 593 // Returns indices: ^ ^ ^ ^ 594 595 let topLevelNodeModulesIndex = 0; 596 let topLevelPackageNameIndex = 0; 597 let packageRootIndex = 0; 598 let fileNameIndex = 0; 599 600 const enum States { 601 BeforeNodeModules, 602 NodeModules, 603 Scope, 604 PackageContent 605 } 606 607 let partStart = 0; 608 let partEnd = 0; 609 let state = States.BeforeNodeModules; 610 611 while (partEnd >= 0) { 612 partStart = partEnd; 613 partEnd = fullPath.indexOf("/", partStart + 1); 614 switch (state) { 615 case States.BeforeNodeModules: 616 if (fullPath.indexOf(modulesPathPart, partStart) === partStart) { 617 topLevelNodeModulesIndex = partStart; 618 topLevelPackageNameIndex = partEnd; 619 state = States.NodeModules; 620 } 621 break; 622 case States.NodeModules: 623 case States.Scope: 624 if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") { 625 state = States.Scope; 626 } 627 else { 628 packageRootIndex = partEnd; 629 state = States.PackageContent; 630 } 631 break; 632 case States.PackageContent: 633 if (fullPath.indexOf(modulesPathPart, partStart) === partStart) { 634 state = States.NodeModules; 635 } 636 else { 637 state = States.PackageContent; 638 } 639 break; 640 } 641 } 642 643 fileNameIndex = partStart; 644 645 return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined; 646 } 647 648 function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string | undefined { 649 return firstDefined(rootDirs, rootDir => { 650 const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)!; // TODO: GH#18217 651 return isPathRelativeToParent(relativePath) ? undefined : relativePath; 652 }); 653 } 654 655 function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string { 656 if (fileExtensionIs(fileName, Extension.Json)) return fileName; 657 const noExtension = removeFileExtension(fileName); 658 switch (ending) { 659 case Ending.Minimal: 660 return removeSuffix(noExtension, "/index"); 661 case Ending.Index: 662 return noExtension; 663 case Ending.JsExtension: 664 return noExtension + getJSExtensionForFile(fileName, options); 665 default: 666 return Debug.assertNever(ending); 667 } 668 } 669 670 function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension { 671 const ext = extensionFromPath(fileName); 672 switch (ext) { 673 case Extension.Ts: 674 case Extension.Dts: 675 case Extension.Ets: 676 case Extension.Dets: 677 return Extension.Js; 678 case Extension.Tsx: 679 return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js; 680 case Extension.Js: 681 case Extension.Jsx: 682 case Extension.Json: 683 return ext; 684 case Extension.TsBuildInfo: 685 return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported:: FileName:: ${fileName}`); 686 default: 687 return Debug.assertNever(ext); 688 } 689 } 690 691 function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { 692 const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); 693 return isRootedDiskPath(relativePath) ? undefined : relativePath; 694 } 695 696 function isPathRelativeToParent(path: string): boolean { 697 return startsWith(path, ".."); 698 } 699} 700