// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers. /* @internal */ namespace ts.moduleSpecifiers { const enum RelativePreference { Relative, NonRelative, Shortest, ExternalNonRelative } // See UserPreferences#importPathEnding const enum Ending { Minimal, Index, JsExtension } // Processed preferences interface Preferences { readonly relativePreference: RelativePreference; readonly ending: Ending; } function getPreferences({ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences { return { relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : importModuleSpecifierPreference === "project-relative" ? RelativePreference.ExternalNonRelative : RelativePreference.Shortest, ending: getEnding(), }; function getEnding(): Ending { switch (importModuleSpecifierEnding) { case "minimal": return Ending.Minimal; case "index": return Ending.Index; case "js": return Ending.JsExtension; default: return usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; } } } function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { return { relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, ending: hasJSFileExtension(oldImportSpecifier) ? Ending.JsExtension : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, }; } export function updateModuleSpecifier( compilerOptions: CompilerOptions, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, oldImportSpecifier: string, ): string | undefined { const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier)); if (res === oldImportSpecifier) return undefined; return res; } // Note: importingSourceFile is just for usesJsExtensionOnImports export function getModuleSpecifier( compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, preferences: UserPreferences = {}, ): string { return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferences(preferences, compilerOptions, importingSourceFile)); } export function getModulesPackageName( compilerOptions: CompilerOptions, importingSourceFileName: Path, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, ): string | undefined { const info = getInfo(importingSourceFileName, host); const modulePaths = getAllModulePaths(importingSourceFileName, nodeModulesFileName, host, compilerOptions.packageManagerType); return firstDefined(modulePaths, modulePath => tryGetModuleNameAsExternalModule(modulePath, info, host, compilerOptions, /*packageNameOnly*/ true)); } function getModuleSpecifierWorker( compilerOptions: CompilerOptions, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, preferences: Preferences ): string { const info = getInfo(importingSourceFileName, host); const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host,compilerOptions.packageManagerType); return firstDefined(modulePaths, modulePath => tryGetModuleNameAsExternalModule(modulePath, info, host, compilerOptions)) || getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences); } /** Returns an import for each symlink and for the realpath. */ export function getModuleSpecifiers( moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, ): readonly string[] { const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker); if (ambient) return [ambient]; const info = getInfo(importingSourceFile.path, host); const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol)); const modulePaths = getAllModulePaths(importingSourceFile.path, moduleSourceFile.originalFileName, host, compilerOptions.packageManagerType); const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); const existingSpecifier = forEach(modulePaths, modulePath => forEach( host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), reason => { if (reason.kind !== FileIncludeKind.Import || reason.file !== importingSourceFile.path) return undefined; const specifier = getModuleNameStringLiteralAt(importingSourceFile, reason.index).text; // If the preference is for non relative and the module specifier is relative, ignore it return preferences.relativePreference !== RelativePreference.NonRelative || !pathIsRelative(specifier) ? specifier : undefined; } )); if (existingSpecifier) return [existingSpecifier]; const importedFileIsInNodeModules = some(modulePaths, p => p.isInNodeModules); // Module specifier priority: // 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 // 2. Specifiers generated using "paths" from tsconfig // 3. Non-relative specfiers resulting from a path through node_modules or oh_modules(e.g. "@foo/bar/path/to/file") // 4. Relative paths let modulesSpecifiers: string[] | undefined; let pathsSpecifiers: string[] | undefined; let relativeSpecifiers: string[] | undefined; for (const modulePath of modulePaths) { const specifier = tryGetModuleNameAsExternalModule(modulePath, info, host, compilerOptions); modulesSpecifiers = append(modulesSpecifiers, specifier); if (specifier && modulePath.isRedirect) { // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking. return modulesSpecifiers!; } if (!specifier && !modulePath.isRedirect) { const local = getLocalModuleSpecifier(modulePath.path, info, compilerOptions, host, preferences); if (pathIsBareSpecifier(local)) { pathsSpecifiers = append(pathsSpecifiers, local); } else if (!importedFileIsInNodeModules || modulePath.isInNodeModules) { // Why this extra conditional, not just an `else`? If some path to the file contained // 'node_modules' or 'oh_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"), // that means we had to go through a *sibling's* node_modules or oh_modules, not one we can access directly. // If some path to the file was in node_modules or oh_modules but another was not, this likely indicates that // we have a monorepo structure with symlinks. In this case, the non-node_modules or oh_modules path is // probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package // in a monorepo is probably not portable. So, the module specifier we actually go with will be // the relative path through node_modules or oh_modules, so that the declaration emitter can produce a // portability error. (See declarationEmitReexportedSymlinkReference3) relativeSpecifiers = append(relativeSpecifiers, local); } } } return pathsSpecifiers?.length ? pathsSpecifiers : modulesSpecifiers?.length ? modulesSpecifiers : Debug.checkDefined(relativeSpecifiers); } interface Info { readonly getCanonicalFileName: GetCanonicalFileName; readonly importingSourceFileName: Path readonly sourceDirectory: Path; } // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info { const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); const sourceDirectory = getDirectoryPath(importingSourceFileName); return { getCanonicalFileName, importingSourceFileName, sourceDirectory }; } function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, { ending, relativePreference }: Preferences): string { const { baseUrl, paths, rootDirs } = compilerOptions; const { sourceDirectory, getCanonicalFileName } = info; const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); if (!baseUrl && !paths || relativePreference === RelativePreference.Relative) { return relativePath; } const baseDirectory = getPathsBasePath(compilerOptions, host) || baseUrl!; const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseDirectory, getCanonicalFileName); if (!relativeToBaseUrl) { return relativePath; } const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions); const fromPaths = paths && tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); const nonRelative = fromPaths === undefined && baseUrl !== undefined ? importRelativeToBaseUrl : fromPaths; if (!nonRelative) { return relativePath; } if (relativePreference === RelativePreference.NonRelative) { return nonRelative; } if (relativePreference === RelativePreference.ExternalNonRelative) { const projectDirectory = host.getCurrentDirectory(); const modulePath = toPath(moduleFileName, projectDirectory, getCanonicalFileName); const sourceIsInternal = startsWith(sourceDirectory, projectDirectory); const targetIsInternal = startsWith(modulePath, projectDirectory); if (sourceIsInternal && !targetIsInternal || !sourceIsInternal && targetIsInternal) { // 1. The import path crosses the boundary of the tsconfig.json-containing directory. // // src/ // tsconfig.json // index.ts ------- // lib/ | (path crosses tsconfig.json) // imported.ts <--- // return nonRelative; } const packageManagerType = compilerOptions.packageManagerType; const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, getDirectoryPath(modulePath), packageManagerType); const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory, packageManagerType); if (nearestSourcePackageJson !== nearestTargetPackageJson) { // 2. The importing and imported files are part of different packages. // // packages/a/ // package.json // index.ts -------- // packages/b/ | (path crosses package.json) // package.json | // component.ts <--- // return nonRelative; } return relativePath; } if (relativePreference !== RelativePreference.Shortest) Debug.assertNever(relativePreference); // Prefer a relative import over a baseUrl import if it has fewer components. return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative; } export function countPathComponents(path: string): number { let count = 0; for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) { if (path.charCodeAt(i) === CharacterCodes.slash) count++; } return count; } function usesJsExtensionOnImports({ imports }: SourceFile): boolean { return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false; } function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) { return compareBooleans(b.isRedirect, a.isRedirect) || compareNumberOfDirectorySeparators(a.path, b.path); } function getNearestAncestorDirectoryWithPackageJson(host: ModuleSpecifierResolutionHost, fileName: string, packageManagerType?: string) { if (host.getNearestAncestorDirectoryWithPackageJson) { return host.getNearestAncestorDirectoryWithPackageJson(fileName); } return !!forEachAncestorDirectory(fileName, directory => { return host.fileExists(combinePaths(directory, getPackageJsonByPMType(packageManagerType))) ? true : undefined; }); } export function forEachFileNameOfModule( importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost, preferSymlinks: boolean, cb: (fileName: string, isRedirect: boolean) => T | undefined, isOHModules?: boolean ): T | undefined { const getCanonicalFileName = hostGetCanonicalFileName(host); const cwd = host.getCurrentDirectory(); const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined; const importedPath = toPath(importedFileName, cwd, getCanonicalFileName); const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray; const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects]; const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd)); let shouldFilterIgnoredPaths = !every(targets, containsIgnoredPath); if (!preferSymlinks) { // Symlinks inside ignored paths are already filtered out of the symlink cache, // so we only need to remove them from the realpath filenames. const result = forEach(targets, p => !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) && cb(p, referenceRedirect === p)); if (result) return result; } const links = host.getSymlinkCache ? host.getSymlinkCache() : discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd, isOHModules); const symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath(); const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd); const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => { const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName))); if (!symlinkDirectories) return undefined; // Continue to ancestor directory // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { return false; // Stop search, each ancestor directory will also hit this condition } return forEach(targets, target => { if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { return; } const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); for (const symlinkDirectory of symlinkDirectories) { const option = resolvePath(symlinkDirectory, relative); const result = cb(option, target === referenceRedirect); shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths if (result) return result; } }); }); return result || (preferSymlinks ? forEach(targets, p => shouldFilterIgnoredPaths && containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect)) : undefined); } interface ModulePath { path: string; isInNodeModules: boolean; isRedirect: boolean; } /** * Looks for existing imports that use symlinks to this module. * Symlinks will be returned first so they are preferred over the real path. */ function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost, packageManagerType?: string): readonly ModulePath[] { const cwd = host.getCurrentDirectory(); const getCanonicalFileName = hostGetCanonicalFileName(host); const allFileNames = new Map(); let importedFileFromNodeModules = false; const isOHModules: boolean =isOhpm(packageManagerType); forEachFileNameOfModule( importingFileName, importedFileName, host, /*preferSymlinks*/ true, (path, isRedirect) => { const isInNodeModules = isOhpm(packageManagerType) ? pathContainsOHModules(path) : pathContainsNodeModules(path); allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules }); importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules; // don't return value, so we collect everything }, isOHModules ); // Sort by paths closest to importing file Name directory const sortedPaths: ModulePath[] = []; for ( let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName)); allFileNames.size !== 0; ) { const directoryStart = ensureTrailingDirectorySeparator(directory); let pathsInDirectory: ModulePath[] | undefined; allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => { if (startsWith(path, directoryStart)) { (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules }); allFileNames.delete(fileName); } }); if (pathsInDirectory) { if (pathsInDirectory.length > 1) { pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); } sortedPaths.push(...pathsInDirectory); } const newDirectory = getDirectoryPath(directory); if (newDirectory === directory) break; directory = newDirectory; } if (allFileNames.size) { const remainingPaths = arrayFrom(allFileNames.values()); if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); sortedPaths.push(...remainingPaths); } return sortedPaths; } function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeChecker): string | undefined { const decl = find(moduleSymbol.declarations, d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name))) ) as (ModuleDeclaration & { name: StringLiteral }) | undefined; if (decl) { return decl.name.text; } // the module could be a namespace, which is export through "export=" from an ambient module. /** * declare module "m" { * namespace ns { * class c {} * } * export = ns; * } */ // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m" const ambientModuleDeclareCandidates = mapDefined(moduleSymbol.declarations, d => { if (!isModuleDeclaration(d)) return; const topNamespace = getTopNamespace(d); if (!(topNamespace?.parent?.parent && isModuleBlock(topNamespace.parent) && isAmbientModule(topNamespace.parent.parent) && isSourceFile(topNamespace.parent.parent.parent))) return; const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as __String)?.valueDeclaration as ExportAssignment)?.expression as PropertyAccessExpression | Identifier); if (!exportAssignment) return; const exportSymbol = checker.getSymbolAtLocation(exportAssignment); if (!exportSymbol) return; const originalExportSymbol = exportSymbol?.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol; if (originalExportSymbol === d.symbol) return topNamespace.parent.parent; function getTopNamespace(namespaceDeclaration: ModuleDeclaration) { while (namespaceDeclaration.flags & NodeFlags.NestedNamespace) { namespaceDeclaration = namespaceDeclaration.parent as ModuleDeclaration; } return namespaceDeclaration; } } ); const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (AmbientModuleDeclaration & { name: StringLiteral }) | undefined; if (ambientModuleDeclare) { return ambientModuleDeclare.name.text; } } function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike): string | undefined { for (const key in paths) { for (const patternText of paths[key]) { const pattern = removeFileExtension(normalizePath(patternText)); const indexOfStar = pattern.indexOf("*"); if (indexOfStar !== -1) { const prefix = pattern.substr(0, indexOfStar); const suffix = pattern.substr(indexOfStar + 1); if (relativeToBaseUrl.length >= prefix.length + suffix.length && startsWith(relativeToBaseUrl, prefix) && endsWith(relativeToBaseUrl, suffix) || !suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) { const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length); return key.replace("*", matchedStar); } } else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) { return key; } } } } function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined { const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); if (normalizedTargetPath === undefined) { return undefined; } const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions) : removeFileExtension(relativePath); } function tryGetModuleNameAsExternalModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { if (!host.fileExists || !host.readFile) { return undefined; } const parts: ModulePathParts = getModulePathParts(path, isOhpm(options.packageManagerType) ? ohModulesPathPart : nodeModulesPathPart)!; if (!parts) { return undefined; } // Simplify the full file path to something that can be resolved by Node. let moduleSpecifier = path; let isPackageRootPath = false; if (!packageNameOnly) { let packageRootIndex = parts.packageRootIndex; let moduleFileNameForExtensionless: string | undefined; while (true) { // If the module could be imported by a directory name, use that directory's name const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex); if (packageRootPath) { moduleSpecifier = packageRootPath; isPackageRootPath = true; break; } if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry; // try with next level of directory packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1); if (packageRootIndex === -1) { moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless); break; } } } if (isRedirect && !isPackageRootPath) { return undefined; } const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation(); // Get a path that's relative to node_modules, oh_modules or the importing file's path // if node_modules or oh_modules folder is in this folder or any of its parent folders, no need to keep it. const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)); if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) { return undefined; } // If the module was found in @types, get the actual Node package name const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName); // For classic resolution, only allow importing from or oh_modules/@types, not other node_modules or oh_modules return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName; function tryDirectoryWithPackageJson(packageRootIndex: number) { const packageRootPath = path.substring(0, packageRootIndex); const packageJsonPath = combinePaths(packageRootPath, getPackageJsonByPMType(options.packageManagerType)); let moduleFileToTry = path; if (host.fileExists(packageJsonPath)) { const isOHModules: boolean = isOhpm(options.packageManagerType); const packageJsonContent = isOHModules ? JSON5.parse(host.readFile!(packageJsonPath)!) : JSON.parse(host.readFile!(packageJsonPath)!); const versionPaths = packageJsonContent.typesVersions ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) : undefined; if (versionPaths) { const subModuleName = path.slice(packageRootPath.length + 1); const fromPaths = tryGetModuleNameFromPaths( removeFileExtension(subModuleName), removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), versionPaths.paths ); if (fromPaths !== undefined) { moduleFileToTry = combinePaths(packageRootPath, fromPaths); } } // If the file is the main module, it can be imported by the package name const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; if (isString(mainFileRelative)) { const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) { return { packageRootPath, moduleFileToTry }; } } } return { moduleFileToTry }; } function getExtensionlessFileName(path: string): string { // We still have a file name - remove the extension const fullModulePathWithoutExtension = removeFileExtension(path); // If the file is /index, it can be imported by its directory name // IFF there is not _also_ a file by the same name if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) { return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex); } return fullModulePathWithoutExtension; } } function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) { if (!host.fileExists) return; // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]); for (const e of extensions) { const fullPath = path + e; if (host.fileExists(fullPath)) { return fullPath; } } } interface ModulePathParts { readonly topLevelNodeModulesIndex: number; readonly topLevelPackageNameIndex: number; readonly packageRootIndex: number; readonly fileNameIndex: number; } function getModulePathParts(fullPath: string, modulesPathPart: string): ModulePathParts | undefined { // If fullPath can't be valid module file within node_modules or oh_modules, returns undefined. // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js // Returns indices: ^ ^ ^ ^ let topLevelNodeModulesIndex = 0; let topLevelPackageNameIndex = 0; let packageRootIndex = 0; let fileNameIndex = 0; const enum States { BeforeNodeModules, NodeModules, Scope, PackageContent } let partStart = 0; let partEnd = 0; let state = States.BeforeNodeModules; while (partEnd >= 0) { partStart = partEnd; partEnd = fullPath.indexOf("/", partStart + 1); switch (state) { case States.BeforeNodeModules: if (fullPath.indexOf(modulesPathPart, partStart) === partStart) { topLevelNodeModulesIndex = partStart; topLevelPackageNameIndex = partEnd; state = States.NodeModules; } break; case States.NodeModules: case States.Scope: if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") { state = States.Scope; } else { packageRootIndex = partEnd; state = States.PackageContent; } break; case States.PackageContent: if (fullPath.indexOf(modulesPathPart, partStart) === partStart) { state = States.NodeModules; } else { state = States.PackageContent; } break; } } fileNameIndex = partStart; return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined; } function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string | undefined { return firstDefined(rootDirs, rootDir => { const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)!; // TODO: GH#18217 return isPathRelativeToParent(relativePath) ? undefined : relativePath; }); } function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string { if (fileExtensionIs(fileName, Extension.Json)) return fileName; const noExtension = removeFileExtension(fileName); switch (ending) { case Ending.Minimal: return removeSuffix(noExtension, "/index"); case Ending.Index: return noExtension; case Ending.JsExtension: return noExtension + getJSExtensionForFile(fileName, options); default: return Debug.assertNever(ending); } } function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension { const ext = extensionFromPath(fileName); switch (ext) { case Extension.Ts: case Extension.Dts: case Extension.Ets: case Extension.Dets: return Extension.Js; case Extension.Tsx: return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js; case Extension.Js: case Extension.Jsx: case Extension.Json: return ext; case Extension.TsBuildInfo: return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported:: FileName:: ${fileName}`); default: return Debug.assertNever(ext); } } function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); return isRootedDiskPath(relativePath) ? undefined : relativePath; } function isPathRelativeToParent(path: string): boolean { return startsWith(path, ".."); } }