2025-10-15 10:12:44 +03:00

1287 lines
52 KiB
Go

package modulespecifiers
import (
"maps"
"slices"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/module"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/outputpaths"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/packagejson"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
func GetModuleSpecifiers(
moduleSymbol *ast.Symbol,
checker CheckerShape,
compilerOptions *core.CompilerOptions,
importingSourceFile SourceFileForSpecifierGeneration,
host ModuleSpecifierGenerationHost,
userPreferences UserPreferences,
options ModuleSpecifierOptions,
forAutoImports bool,
) []string {
result, _ := GetModuleSpecifiersWithInfo(
moduleSymbol,
checker,
compilerOptions,
importingSourceFile,
host,
userPreferences,
options,
forAutoImports,
)
return result
}
func GetModuleSpecifiersWithInfo(
moduleSymbol *ast.Symbol,
checker CheckerShape,
compilerOptions *core.CompilerOptions,
importingSourceFile SourceFileForSpecifierGeneration,
host ModuleSpecifierGenerationHost,
userPreferences UserPreferences,
options ModuleSpecifierOptions,
forAutoImports bool,
) ([]string, ResultKind) {
ambient := tryGetModuleNameFromAmbientModule(moduleSymbol, checker)
if len(ambient) > 0 {
// !!! todo forAutoImport
return []string{ambient}, ResultKindAmbient
}
moduleSourceFile := ast.GetSourceFileOfModule(moduleSymbol)
if moduleSourceFile == nil {
return nil, ResultKindNone
}
modulePaths := getAllModulePathsWorker(
getInfo(host.GetSourceOfProjectReferenceIfOutputIncluded(importingSourceFile), host),
moduleSourceFile.FileName(),
host,
// compilerOptions,
// options,
)
return computeModuleSpecifiers(
modulePaths,
compilerOptions,
importingSourceFile,
host,
userPreferences,
options,
forAutoImports,
)
}
func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker CheckerShape) string {
for _, decl := range moduleSymbol.Declarations {
if isNonGlobalAmbientModule(decl) && (!ast.IsModuleAugmentationExternal(decl) || !tspath.IsExternalModuleNameRelative(decl.Name().AsStringLiteral().Text)) {
return decl.Name().AsStringLiteral().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"
for _, d := range moduleSymbol.Declarations {
if !ast.IsModuleDeclaration(d) {
continue
}
possibleContainer := ast.FindAncestor(d, isNonGlobalAmbientModule)
if possibleContainer == nil || possibleContainer.Parent == nil || !ast.IsSourceFile(possibleContainer.Parent) {
continue
}
sym, ok := possibleContainer.Symbol().Exports[ast.InternalSymbolNameExportEquals]
if !ok || sym == nil {
continue
}
exportAssignmentDecl := sym.ValueDeclaration
if exportAssignmentDecl == nil || exportAssignmentDecl.Kind != ast.KindExportAssignment {
continue
}
exportSymbol := checker.GetSymbolAtLocation(exportAssignmentDecl.Expression())
if exportSymbol == nil {
continue
}
if exportSymbol.Flags&ast.SymbolFlagsAlias != 0 {
exportSymbol = checker.GetAliasedSymbol(exportSymbol)
}
// TODO: Possible strada bug - isn't this insufficient in the presence of merge symbols?
if exportSymbol == d.Symbol() {
return possibleContainer.Name().AsStringLiteral().Text
}
}
return ""
}
type Info struct {
UseCaseSensitiveFileNames bool
ImportingSourceFileName string
SourceDirectory string
}
func getInfo(
importingSourceFileName string,
host ModuleSpecifierGenerationHost,
) Info {
sourceDirectory := tspath.GetDirectoryPath(importingSourceFileName)
return Info{
ImportingSourceFileName: importingSourceFileName,
SourceDirectory: sourceDirectory,
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
}
}
func getAllModulePaths(
info Info,
importedFileName string,
host ModuleSpecifierGenerationHost,
compilerOptions *core.CompilerOptions,
preferences UserPreferences,
options ModuleSpecifierOptions,
) []ModulePath {
// !!! use new cache model
// importingFilePath := tspath.ToPath(info.ImportingSourceFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames());
// importedFilePath := tspath.ToPath(importedFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames());
// cache := host.getModuleSpecifierCache();
// if (cache != nil) {
// cached := cache.get(importingFilePath, importedFilePath, preferences, options);
// if (cached.modulePaths) {return cached.modulePaths;}
// }
modulePaths := getAllModulePathsWorker(info, importedFileName, host) // , compilerOptions, options);
// if (cache != nil) {
// cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths);
// }
return modulePaths
}
func getAllModulePathsWorker(
info Info,
importedFileName string,
host ModuleSpecifierGenerationHost,
// compilerOptions *core.CompilerOptions,
// options ModuleSpecifierOptions,
) []ModulePath {
// !!! TODO: Caches and symlink cache chicanery to support pulling in non-explicit package.json dep names
// cache := host.GetModuleResolutionCache() // !!!
// links := host.GetSymlinkCache() // !!!
// if cache != nil && links != nil && !strings.Contains(info.ImportingSourceFileName, "/node_modules/") {
// // Debug.type<ModuleResolutionHost>(host); // !!!
// // Cache resolutions for all `dependencies` of the `package.json` context of the input file.
// // This should populate all the relevant symlinks in the symlink cache, and most, if not all, of these resolutions
// // should get (re)used.
// // const state = getTemporaryModuleResolutionState(cache.getPackageJsonInfoCache(), host, {});
// // const packageJson = getPackageScopeForPath(getDirectoryPath(info.importingSourceFileName), state);
// // if (packageJson) {
// // const toResolve = getAllRuntimeDependencies(packageJson.contents.packageJsonContent);
// // for (const depName of (toResolve || emptyArray)) {
// // const resolved = resolveModuleName(depName, combinePaths(packageJson.packageDirectory, "package.json"), compilerOptions, host, cache, /*redirectedReference*/ undefined, options.overrideImportMode);
// // links.setSymlinksFromResolution(resolved.resolvedModule);
// // }
// // }
// }
allFileNames := make(map[string]ModulePath)
paths := GetEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true)
for _, p := range paths {
allFileNames[p.FileName] = p
}
// Sort by paths closest to importing file Name directory
sortedPaths := make([]ModulePath, 0, len(paths))
for directory := info.SourceDirectory; len(allFileNames) != 0; {
directoryStart := tspath.EnsureTrailingDirectorySeparator(directory)
var pathsInDirectory []ModulePath
for fileName, p := range allFileNames {
if strings.HasPrefix(fileName, directoryStart) {
pathsInDirectory = append(pathsInDirectory, p)
delete(allFileNames, fileName)
}
}
if len(pathsInDirectory) > 0 {
slices.SortStableFunc(pathsInDirectory, comparePathsByRedirectAndNumberOfDirectorySeparators)
sortedPaths = append(sortedPaths, pathsInDirectory...)
}
newDirectory := tspath.GetDirectoryPath(directory)
if newDirectory == directory {
break
}
directory = newDirectory
}
if len(allFileNames) > 0 {
remainingPaths := slices.Collect(maps.Values(allFileNames))
slices.SortStableFunc(remainingPaths, comparePathsByRedirectAndNumberOfDirectorySeparators)
sortedPaths = append(sortedPaths, remainingPaths...)
}
return sortedPaths
}
func containsIgnoredPath(s string) bool {
return strings.Contains(s, "/node_modules/.") ||
strings.Contains(s, "/.git") ||
strings.Contains(s, "/.#")
}
func ContainsNodeModules(s string) bool {
return strings.Contains(s, "/node_modules/")
}
func GetEachFileNameOfModule(
importingFileName string,
importedFileName string,
host ModuleSpecifierGenerationHost,
preferSymlinks bool,
) []ModulePath {
cwd := host.GetCurrentDirectory()
importedPath := tspath.ToPath(importedFileName, cwd, host.UseCaseSensitiveFileNames())
var referenceRedirect string
outputAndReference := host.GetProjectReferenceFromSource(importedPath)
if outputAndReference != nil && outputAndReference.OutputDts != "" {
referenceRedirect = outputAndReference.OutputDts
}
redirects := host.GetRedirectTargets(importedPath)
importedFileNames := make([]string, 0, 2+len(redirects))
if len(referenceRedirect) > 0 {
importedFileNames = append(importedFileNames, referenceRedirect)
}
importedFileNames = append(importedFileNames, importedFileName)
importedFileNames = append(importedFileNames, redirects...)
targets := core.Map(importedFileNames, func(f string) string { return tspath.GetNormalizedAbsolutePath(f, cwd) })
shouldFilterIgnoredPaths := !core.Every(targets, containsIgnoredPath)
results := make([]ModulePath, 0, 2)
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.
for _, p := range targets {
if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) {
results = append(results, ModulePath{
FileName: p,
IsInNodeModules: ContainsNodeModules(p),
IsRedirect: referenceRedirect == p,
})
}
}
}
// !!! TODO: Symlink directory handling
// const symlinkedDirectories = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath();
// const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd);
// const result = symlinkedDirectories && forEachAncestorDirectoryStoppingAtGlobalCache(
// host,
// 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;
// }
// });
// },
// );
if preferSymlinks {
for _, p := range targets {
if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) {
results = append(results, ModulePath{
FileName: p,
IsInNodeModules: ContainsNodeModules(p),
IsRedirect: referenceRedirect == p,
})
}
}
}
return results
}
func computeModuleSpecifiers(
modulePaths []ModulePath,
compilerOptions *core.CompilerOptions,
importingSourceFile SourceFileForSpecifierGeneration,
host ModuleSpecifierGenerationHost,
userPreferences UserPreferences,
options ModuleSpecifierOptions,
forAutoImport bool,
) ([]string, ResultKind) {
info := getInfo(importingSourceFile.FileName(), host)
preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, "")
var existingSpecifier string
for _, modulePath := range modulePaths {
targetPath := tspath.ToPath(modulePath.FileName, host.GetCurrentDirectory(), info.UseCaseSensitiveFileNames)
var existingImport *ast.StringLiteralLike
for _, importSpecifier := range importingSourceFile.Imports() {
resolvedModule := host.GetResolvedModuleFromModuleSpecifier(importingSourceFile, importSpecifier)
if resolvedModule.IsResolved() && tspath.ToPath(resolvedModule.ResolvedFileName, host.GetCurrentDirectory(), info.UseCaseSensitiveFileNames) == targetPath {
existingImport = importSpecifier
break
}
}
if existingImport != nil {
if preferences.relativePreference == RelativePreferenceNonRelative && tspath.PathIsRelative(existingImport.Text()) {
// If the preference is for non-relative and the module specifier is relative, ignore it
continue
}
existingMode := host.GetModeForUsageLocation(importingSourceFile, existingImport)
targetMode := options.OverrideImportMode
if targetMode == core.ResolutionModeNone {
targetMode = host.GetDefaultResolutionModeForFile(importingSourceFile)
}
if existingMode != targetMode && existingMode != core.ResolutionModeNone && targetMode != core.ResolutionModeNone {
// If the candidate import mode doesn't match the mode we're generating for, don't consider it
continue
}
existingSpecifier = existingImport.Text()
break
}
}
if existingSpecifier != "" {
return []string{existingSpecifier}, ResultKindNone
}
importedFileIsInNodeModules := core.Some(modulePaths, func(p ModulePath) bool { return p.IsInNodeModules })
// Module specifier priority:
// 1. "Bare package specifiers" (e.g. "@foo/bar") resulting from a path through node_modules to a package.json's "types" entry
// 2. Specifiers generated using "paths" from tsconfig
// 3. Non-relative specfiers resulting from a path through node_modules (e.g. "@foo/bar/path/to/file")
// 4. Relative paths
var pathsSpecifiers []string
var redirectPathsSpecifiers []string
var nodeModulesSpecifiers []string
var relativeSpecifiers []string
for _, modulePath := range modulePaths {
var specifier string
if modulePath.IsInNodeModules {
specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences /*packageNameOnly*/, false, options.OverrideImportMode)
}
if len(specifier) > 0 && !(forAutoImport && isExcludedByRegex(specifier, preferences.excludeRegexes)) {
nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier)
if 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 nodeModulesSpecifiers, ResultKindNodeModules
}
}
importMode := options.OverrideImportMode
if importMode == core.ResolutionModeNone {
importMode = host.GetDefaultResolutionModeForFile(importingSourceFile)
}
local := getLocalModuleSpecifier(
modulePath.FileName,
info,
compilerOptions,
host,
importMode,
preferences,
/*pathsOnly*/ modulePath.IsRedirect || len(specifier) > 0,
)
if len(local) == 0 || forAutoImport && isExcludedByRegex(local, preferences.excludeRegexes) {
continue
}
if modulePath.IsRedirect {
redirectPathsSpecifiers = append(redirectPathsSpecifiers, local)
} else if PathIsBareSpecifier(local) {
if ContainsNodeModules(local) {
// We could be in this branch due to inappropriate use of `baseUrl`, not intentional `paths`
// usage. It's impossible to reason about where to prioritize baseUrl-generated module
// specifiers, but if they contain `/node_modules/`, they're going to trigger a portability
// error, so *at least* don't prioritize those.
relativeSpecifiers = append(relativeSpecifiers, local)
} else {
pathsSpecifiers = append(pathsSpecifiers, local)
}
} else if forAutoImport || !importedFileIsInNodeModules || modulePath.IsInNodeModules {
// Why this extra conditional, not just an `else`? If some path to the file contained
// 'node_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, not one we can access directly.
// If some path to the file was in node_modules but another was not, this likely indicates that
// we have a monorepo structure with symlinks. In this case, the non-nodeModules 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, so that the declaration emitter can produce a
// portability error. (See declarationEmitReexportedSymlinkReference3)
relativeSpecifiers = append(relativeSpecifiers, local)
}
}
if len(pathsSpecifiers) > 0 {
return pathsSpecifiers, ResultKindPaths
}
if len(redirectPathsSpecifiers) > 0 {
return redirectPathsSpecifiers, ResultKindRedirect
}
if len(nodeModulesSpecifiers) > 0 {
return nodeModulesSpecifiers, ResultKindNodeModules
}
return relativeSpecifiers, ResultKindRelative
}
func getLocalModuleSpecifier(
moduleFileName string,
info Info,
compilerOptions *core.CompilerOptions,
host ModuleSpecifierGenerationHost,
importMode core.ResolutionMode,
preferences ModuleSpecifierPreferences,
pathsOnly bool,
) string {
paths := compilerOptions.Paths
rootDirs := compilerOptions.RootDirs
if pathsOnly && paths == nil {
return ""
}
sourceDirectory := info.SourceDirectory
allowedEndings := preferences.getAllowedEndingsInPreferredOrder(importMode)
var relativePath string
if len(rootDirs) > 0 {
relativePath = tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, allowedEndings, compilerOptions, host)
}
if len(relativePath) == 0 {
relativePath = processEnding(ensurePathIsNonModuleName(tspath.GetRelativePathFromDirectory(sourceDirectory, moduleFileName, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
})), allowedEndings, compilerOptions, host)
}
root := compilerOptions.GetPathsBasePath(host.GetCurrentDirectory())
baseDirectory := tspath.GetNormalizedAbsolutePath(root, host.GetCurrentDirectory())
relativeToBaseUrl := getRelativePathIfInSameVolume(moduleFileName, baseDirectory, host.UseCaseSensitiveFileNames())
if len(relativeToBaseUrl) == 0 {
if pathsOnly {
return ""
}
return relativePath
}
var fromPackageJsonImports string
if !pathsOnly {
fromPackageJsonImports = tryGetModuleNameFromPackageJsonImports(
moduleFileName,
sourceDirectory,
compilerOptions,
host,
importMode,
prefersTsExtension(allowedEndings),
)
}
var fromPaths string
if (pathsOnly || len(fromPackageJsonImports) == 0) && paths != nil {
fromPaths = tryGetModuleNameFromPaths(
relativeToBaseUrl,
paths,
allowedEndings,
baseDirectory,
host,
compilerOptions,
)
}
if pathsOnly {
return fromPaths
}
var maybeNonRelative string
if len(fromPackageJsonImports) > 0 {
maybeNonRelative = fromPackageJsonImports
} else {
maybeNonRelative = fromPaths
}
if len(maybeNonRelative) == 0 {
return relativePath
}
relativeIsExcluded := isExcludedByRegex(relativePath, preferences.excludeRegexes)
nonRelativeIsExcluded := isExcludedByRegex(maybeNonRelative, preferences.excludeRegexes)
if !relativeIsExcluded && nonRelativeIsExcluded {
return relativePath
}
if relativeIsExcluded && !nonRelativeIsExcluded {
return maybeNonRelative
}
if preferences.relativePreference == RelativePreferenceNonRelative && !tspath.PathIsRelative(maybeNonRelative) {
return maybeNonRelative
}
if preferences.relativePreference == RelativePreferenceExternalNonRelative && !tspath.PathIsRelative(maybeNonRelative) {
var projectDirectory tspath.Path
if len(compilerOptions.ConfigFilePath) > 0 {
projectDirectory = tspath.ToPath(compilerOptions.ConfigFilePath, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames())
} else {
projectDirectory = tspath.ToPath(host.GetCurrentDirectory(), host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames())
}
canonicalSourceDirectory := tspath.ToPath(sourceDirectory, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames())
modulePath := tspath.ToPath(moduleFileName, string(projectDirectory), host.UseCaseSensitiveFileNames())
sourceIsInternal := strings.HasPrefix(string(canonicalSourceDirectory), string(projectDirectory))
targetIsInternal := strings.HasPrefix(string(modulePath), string(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 maybeNonRelative
}
nearestTargetPackageJson := host.GetNearestAncestorDirectoryWithPackageJson(tspath.GetDirectoryPath(string(modulePath)))
nearestSourcePackageJson := host.GetNearestAncestorDirectoryWithPackageJson(sourceDirectory)
if !packageJsonPathsAreEqual(nearestTargetPackageJson, nearestSourcePackageJson, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
}) {
// 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 maybeNonRelative
}
}
// Prefer a relative import over a baseUrl import if it has fewer components.
if isPathRelativeToParent(maybeNonRelative) || strings.Count(relativePath, "/") < strings.Count(maybeNonRelative, "/") {
return relativePath
}
return maybeNonRelative
}
func processEnding(
fileName string,
allowedEndings []ModuleSpecifierEnding,
options *core.CompilerOptions,
host ModuleSpecifierGenerationHost,
) string {
if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionJson, tspath.ExtensionMjs, tspath.ExtensionCjs}) {
return fileName
}
noExtension := tspath.RemoveFileExtension(fileName)
if fileName == noExtension {
return fileName
}
jsPriority := slices.Index(allowedEndings, ModuleSpecifierEndingJsExtension)
tsPriority := slices.Index(allowedEndings, ModuleSpecifierEndingTsExtension)
if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionCts}) && tsPriority < jsPriority {
return fileName
}
if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionDcts, tspath.ExtensionCts}) {
inputExt := tspath.GetDeclarationFileExtension(fileName)
ext := getJsExtensionForDeclarationFileExtension(inputExt)
return tspath.RemoveExtension(fileName, inputExt) + ext
}
switch allowedEndings[0] {
case ModuleSpecifierEndingMinimal:
withoutIndex := strings.TrimSuffix(noExtension, "/index")
if host != nil && withoutIndex != noExtension && tryGetAnyFileFromPath(host, withoutIndex) {
// Can't remove index if there's a file by the same name as the directory.
// Probably more callers should pass `host` so we can determine this?
return noExtension
}
return withoutIndex
case ModuleSpecifierEndingIndex:
return noExtension
case ModuleSpecifierEndingJsExtension:
return noExtension + getJSExtensionForFile(fileName, options)
case ModuleSpecifierEndingTsExtension:
// declaration files are already handled first with a remap back to input js paths,
// and mjs/cjs/json are already singled out,
// so we know fileName has to be either an input .js or .ts path already
// TODO: possible dead code in strada in this branch to do with declaration file name handling
return fileName
default:
debug.AssertNever(allowedEndings[0])
return ""
}
}
func tryGetModuleNameFromRootDirs(
rootDirs []string,
moduleFileName string,
sourceDirectory string,
allowedEndings []ModuleSpecifierEnding,
compilerOptions *core.CompilerOptions,
host ModuleSpecifierGenerationHost,
) string {
normalizedTargetPaths := getPathsRelativeToRootDirs(moduleFileName, rootDirs, host.UseCaseSensitiveFileNames())
if len(normalizedTargetPaths) == 0 {
return ""
}
normalizedSourcePaths := getPathsRelativeToRootDirs(sourceDirectory, rootDirs, host.UseCaseSensitiveFileNames())
var shortest string
var shortestSepCount int
for _, sourcePath := range normalizedSourcePaths {
for _, targetPath := range normalizedTargetPaths {
candidate := ensurePathIsNonModuleName(tspath.GetRelativePathFromDirectory(sourcePath, targetPath, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
}))
candidateSepCount := strings.Count(candidate, "/")
if len(shortest) == 0 || candidateSepCount < shortestSepCount {
shortest = candidate
shortestSepCount = candidateSepCount
}
}
}
if len(shortest) == 0 {
return ""
}
return processEnding(shortest, allowedEndings, compilerOptions, host)
}
func tryGetModuleNameAsNodeModule(
pathObj ModulePath,
info Info,
importingSourceFile SourceFileForSpecifierGeneration,
host ModuleSpecifierGenerationHost,
options *core.CompilerOptions,
userPreferences UserPreferences,
packageNameOnly bool,
overrideMode core.ResolutionMode,
) string {
parts := GetNodeModulePathParts(pathObj.FileName)
if parts == nil {
return ""
}
// Simplify the full file path to something that can be resolved by Node.
preferences := getModuleSpecifierPreferences(userPreferences, host, options, importingSourceFile, "")
allowedEndings := preferences.getAllowedEndingsInPreferredOrder(core.ResolutionModeNone)
caseSensitive := host.UseCaseSensitiveFileNames()
moduleSpecifier := pathObj.FileName
isPackageRootPath := false
if !packageNameOnly {
packageRootIndex := parts.PackageRootIndex
var moduleFileName string
for true {
// If the module could be imported by a directory name, use that directory's name
pkgJsonResults := tryDirectoryWithPackageJson(
*parts,
pathObj,
importingSourceFile,
host,
overrideMode,
options,
allowedEndings,
)
moduleFileToTry := pkgJsonResults.moduleFileToTry
packageRootPath := pkgJsonResults.packageRootPath
blockedByExports := pkgJsonResults.blockedByExports
verbatimFromExports := pkgJsonResults.verbatimFromExports
if blockedByExports {
return "" // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution
}
if verbatimFromExports {
return moduleFileToTry
}
//}
if len(packageRootPath) > 0 {
moduleSpecifier = packageRootPath
isPackageRootPath = true
break
}
if len(moduleFileName) == 0 {
moduleFileName = moduleFileToTry
}
// try with next level of directory
packageRootIndex = core.IndexAfter(pathObj.FileName, "/", packageRootIndex+1)
if packageRootIndex == -1 {
moduleSpecifier = processEnding(moduleFileName, allowedEndings, options, host)
break
}
}
}
if pathObj.IsRedirect && !isPackageRootPath {
return ""
}
globalTypingsCacheLocation := host.GetGlobalTypingsCacheLocation()
// Get a path that's relative to node_modules or the importing file's path
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
pathToTopLevelNodeModules := moduleSpecifier[0:parts.TopLevelNodeModulesIndex]
if !stringutil.HasPrefix(info.SourceDirectory, pathToTopLevelNodeModules, caseSensitive) || len(globalTypingsCacheLocation) > 0 && stringutil.HasPrefix(globalTypingsCacheLocation, pathToTopLevelNodeModules, caseSensitive) {
return ""
}
// If the module was found in @types, get the actual Node package name
nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:]
return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName)
}
type pkgJsonDirAttemptResult struct {
moduleFileToTry string
packageRootPath string
blockedByExports bool
verbatimFromExports bool
}
func tryDirectoryWithPackageJson(
parts NodeModulePathParts,
pathObj ModulePath,
importingSourceFile SourceFileForSpecifierGeneration,
host ModuleSpecifierGenerationHost,
overrideMode core.ResolutionMode,
options *core.CompilerOptions,
allowedEndings []ModuleSpecifierEnding,
) pkgJsonDirAttemptResult {
rootIdx := parts.PackageRootIndex
if rootIdx == -1 {
rootIdx = len(pathObj.FileName) // TODO: possible strada bug? -1 in js slice removes characters from the end, in go it panics - js behavior seems unwanted here?
}
packageRootPath := pathObj.FileName[0:rootIdx]
packageJsonPath := tspath.CombinePaths(packageRootPath, "package.json")
moduleFileToTry := pathObj.FileName
maybeBlockedByTypesVersions := false
packageJson := host.GetPackageJsonInfo(packageJsonPath)
if packageJson == nil {
// No package.json exists; an index.js will still resolve as the package name
fileName := moduleFileToTry[parts.PackageRootIndex+1:]
if fileName == "index.d.ts" || fileName == "index.js" || fileName == "index.ts" || fileName == "index.tsx" {
return pkgJsonDirAttemptResult{moduleFileToTry: moduleFileToTry, packageRootPath: packageRootPath}
}
}
importMode := overrideMode
if importMode == core.ResolutionModeNone {
importMode = host.GetDefaultResolutionModeForFile(importingSourceFile)
}
var packageJsonContent *packagejson.PackageJson
if packageJson != nil {
packageJsonContent = packageJson.GetContents()
}
if options.GetResolvePackageJsonImports() {
// The package name that we found in node_modules could be different from the package
// name in the package.json content via url/filepath dependency specifiers. We need to
// use the actual directory name, so don't look at `packageJsonContent.name` here.
nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:]
packageName := GetPackageNameFromTypesPackageName(nodeModulesDirectoryName)
conditions := module.GetConditions(options, importMode)
var fromExports string
if packageJsonContent != nil && packageJsonContent.Fields.Exports.Type != packagejson.JSONValueTypeNotPresent {
fromExports = tryGetModuleNameFromExports(
options,
host,
pathObj.FileName,
packageRootPath,
packageName,
packageJsonContent.Fields.Exports,
conditions,
)
}
if len(fromExports) > 0 {
return pkgJsonDirAttemptResult{
moduleFileToTry: fromExports,
verbatimFromExports: true,
}
}
if packageJsonContent != nil && packageJsonContent.Fields.Exports.Type != packagejson.JSONValueTypeNotPresent {
return pkgJsonDirAttemptResult{
moduleFileToTry: pathObj.FileName,
blockedByExports: true,
}
}
}
var versionPaths packagejson.VersionPaths
if packageJsonContent != nil && packageJsonContent.TypesVersions.Type == packagejson.JSONValueTypeObject {
versionPaths = packageJsonContent.GetVersionPaths(nil)
}
if versionPaths.GetPaths() != nil {
subModuleName := pathObj.FileName[len(packageRootPath)+1:]
fromPaths := tryGetModuleNameFromPaths(
subModuleName,
versionPaths.GetPaths(),
allowedEndings,
packageRootPath,
host,
options,
)
if len(fromPaths) == 0 {
maybeBlockedByTypesVersions = true
} else {
moduleFileToTry = tspath.CombinePaths(packageRootPath, fromPaths)
}
}
// If the file is the main module, it can be imported by the package name
mainFileRelative := "index.js"
if packageJsonContent != nil {
if packageJsonContent.Typings.Valid {
mainFileRelative = packageJsonContent.Typings.Value
} else if packageJsonContent.Types.Valid {
mainFileRelative = packageJsonContent.Types.Value
} else if packageJsonContent.Main.Valid {
mainFileRelative = packageJsonContent.Main.Value
}
}
if len(mainFileRelative) > 0 && !(maybeBlockedByTypesVersions && module.MatchPatternOrExact(module.TryParsePatterns(versionPaths.GetPaths()), mainFileRelative) != core.Pattern{}) {
// The 'main' file is also subject to mapping through typesVersions, and we couldn't come up with a path
// explicitly through typesVersions, so if it matches a key in typesVersions now, it's not reachable.
// (The only way this can happen is if some file in a package that's not resolvable from outside the
// package got pulled into the program anyway, e.g. transitively through a file that *is* reachable. It
// happens very easily in fourslash tests though, since every test file listed gets included. See
// importNameCodeFix_typesVersions.ts for an example.)
mainExportFile := tspath.ToPath(mainFileRelative, packageRootPath, host.UseCaseSensitiveFileNames())
compareOpt := tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
}
if tspath.ComparePaths(tspath.RemoveFileExtension(string(mainExportFile)), tspath.RemoveFileExtension(moduleFileToTry), compareOpt) == 0 {
// ^ An arbitrary removal of file extension for this comparison is almost certainly wrong
return pkgJsonDirAttemptResult{packageRootPath: packageRootPath, moduleFileToTry: moduleFileToTry}
} else if packageJsonContent == nil || packageJsonContent.Type.Value != "module" &&
!tspath.FileExtensionIsOneOf(moduleFileToTry, tspath.ExtensionsNotSupportingExtensionlessResolution) &&
stringutil.HasPrefix(moduleFileToTry, string(mainExportFile), host.UseCaseSensitiveFileNames()) &&
tspath.ComparePaths(tspath.GetDirectoryPath(moduleFileToTry), tspath.RemoveTrailingDirectorySeparator(string(mainExportFile)), compareOpt) == 0 &&
tspath.RemoveFileExtension(tspath.GetBaseFileName(moduleFileToTry)) == "index" {
// if mainExportFile is a directory, which contains moduleFileToTry, we just try index file
// example mainExportFile: `pkg/lib` and moduleFileToTry: `pkg/lib/index`, we can use packageRootPath
// but this behavior is deprecated for packages with "type": "module", so we only do this for packages without "type": "module"
// and make sure that the extension on index.{???} is something that supports omitting the extension
return pkgJsonDirAttemptResult{packageRootPath: packageRootPath, moduleFileToTry: moduleFileToTry}
}
}
return pkgJsonDirAttemptResult{moduleFileToTry: moduleFileToTry}
}
func tryGetModuleNameFromExports(
options *core.CompilerOptions,
host ModuleSpecifierGenerationHost,
targetFilePath string,
packageDirectory string,
packageName string,
exports packagejson.ExportsOrImports,
conditions []string,
) string {
if exports.IsSubpaths() {
// sub-mappings
// 3 cases:
// * directory mappings (legacyish, key ends with / (technically allows index/extension resolution under cjs mode))
// * pattern mappings (contains a *)
// * exact mappings (no *, does not end with /)
for k, subk := range exports.AsObject().Entries() {
subPackageName := tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(packageName, k), "")
mode := MatchingModeExact
if strings.HasSuffix(k, "/") {
mode = MatchingModeDirectory
} else if strings.Contains(k, "*") {
mode = MatchingModePattern
}
result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, subPackageName, subk, conditions, mode /*isImports*/, false /*preferTsExtension*/, false)
if len(result) > 0 {
return result
}
}
}
return tryGetModuleNameFromExportsOrImports(
options,
host,
targetFilePath,
packageDirectory,
packageName,
exports,
conditions,
MatchingModeExact,
/*isImports*/ false,
/*preferTsExtension*/ false,
)
}
func tryGetModuleNameFromPackageJsonImports(
moduleFileName string,
sourceDirectory string,
options *core.CompilerOptions,
host ModuleSpecifierGenerationHost,
importMode core.ResolutionMode,
preferTsExtension bool,
) string {
if !options.GetResolvePackageJsonImports() {
return ""
}
ancestorDirectoryWithPackageJson := host.GetNearestAncestorDirectoryWithPackageJson(sourceDirectory)
if len(ancestorDirectoryWithPackageJson) == 0 {
return ""
}
packageJsonPath := tspath.CombinePaths(ancestorDirectoryWithPackageJson, "package.json")
info := host.GetPackageJsonInfo(packageJsonPath)
if info == nil {
return ""
}
imports := info.GetContents().Fields.Imports
switch imports.Type {
case packagejson.JSONValueTypeNotPresent, packagejson.JSONValueTypeArray, packagejson.JSONValueTypeString:
return "" // not present or invalid for imports
case packagejson.JSONValueTypeObject:
conditions := module.GetConditions(options, importMode)
top := imports.AsObject()
entries := top.Entries()
for k, value := range entries {
if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") {
continue // invalid imports entry
}
mode := MatchingModeExact
if strings.HasSuffix(k, "/") {
mode = MatchingModeDirectory
} else if strings.Contains(k, "*") {
mode = MatchingModePattern
}
result := tryGetModuleNameFromExportsOrImports(
options,
host,
moduleFileName,
ancestorDirectoryWithPackageJson,
k,
value,
conditions,
mode,
true,
preferTsExtension,
)
if len(result) > 0 {
return result
}
}
}
return ""
}
type specPair struct {
ending ModuleSpecifierEnding
value string
}
func tryGetModuleNameFromPaths(
relativeToBaseUrl string,
paths *collections.OrderedMap[string, []string],
allowedEndings []ModuleSpecifierEnding,
baseDirectory string,
host ModuleSpecifierGenerationHost,
compilerOptions *core.CompilerOptions,
) string {
caseSensitive := host.UseCaseSensitiveFileNames()
for key, values := range paths.Entries() {
for _, patternText := range values {
normalized := tspath.NormalizePath(patternText)
pattern := getRelativePathIfInSameVolume(normalized, baseDirectory, caseSensitive)
if len(pattern) == 0 {
pattern = normalized
}
indexOfStar := strings.Index(pattern, "*")
// In module resolution, if `pattern` itself has an extension, a file with that extension is looked up directly,
// meaning a '.ts' or '.d.ts' extension is allowed to resolve. This is distinct from the case where a '*' substitution
// causes a module specifier to have an extension, i.e. the extension comes from the module specifier in a JS/TS file
// and matches the '*'. For example:
//
// Module Specifier | Path Mapping (key: [pattern]) | Interpolation | Resolution Action
// ---------------------->------------------------------->--------------------->---------------------------------------------------------------
// import "@app/foo" -> "@app/*": ["./src/app/*.ts"] -> "./src/app/foo.ts" -> tryFile("./src/app/foo.ts") || [continue resolution algorithm]
// import "@app/foo.ts" -> "@app/*": ["./src/app/*"] -> "./src/app/foo.ts" -> [continue resolution algorithm]
//
// (https://github.com/microsoft/TypeScript/blob/ad4ded80e1d58f0bf36ac16bea71bc10d9f09895/src/compiler/moduleNameResolver.ts#L2509-L2516)
//
// The interpolation produced by both scenarios is identical, but only in the former, where the extension is encoded in
// the path mapping rather than in the module specifier, will we prioritize a file lookup on the interpolation result.
// (In fact, currently, the latter scenario will necessarily fail since no resolution mode recognizes '.ts' as a valid
// extension for a module specifier.)
//
// Here, this means we need to be careful about whether we generate a match from the target filename (typically with a
// .ts extension) or the possible relative module specifiers representing that file:
//
// Filename | Relative Module Specifier Candidates | Path Mapping | Filename Result | Module Specifier Results
// --------------------<----------------------------------------------<------------------------------<-------------------||----------------------------
// dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*.d.ts"] <- @app/haha || (none)
// dist/haha.d.ts <- dist/haha, dist/haha.js <- "@app/*": ["./dist/*"] <- (none) || @app/haha, @app/haha.js
// dist/foo/index.d.ts <- dist/foo, dist/foo/index, dist/foo/index.js <- "@app/*": ["./dist/*.d.ts"] <- @app/foo/index || (none)
// dist/foo/index.d.ts <- dist/foo, dist/foo/index, dist/foo/index.js <- "@app/*": ["./dist/*"] <- (none) || @app/foo, @app/foo/index, @app/foo/index.js
// dist/wow.js.js <- dist/wow.js, dist/wow.js.js <- "@app/*": ["./dist/*.js"] <- @app/wow.js || @app/wow, @app/wow.js
//
// The "Filename Result" can be generated only if `pattern` has an extension. Care must be taken that the list of
// relative module specifiers to run the interpolation (a) is actually valid for the module resolution mode, (b) takes
// into account the existence of other files (e.g. 'dist/wow.js' cannot refer to 'dist/wow.js.js' if 'dist/wow.js'
// exists) and (c) that they are ordered by preference. The last row shows that the filename result and module
// specifier results are not mutually exclusive. Note that the filename result is a higher priority in module
// resolution, but as long criteria (b) above is met, I don't think its result needs to be the highest priority result
// in module specifier generation. I have included it last, as it's difficult to tell exactly where it should be
// sorted among the others for a particular value of `importModuleSpecifierEnding`.
var candidates []specPair
for _, ending := range allowedEndings {
result := processEnding(
relativeToBaseUrl,
[]ModuleSpecifierEnding{ending},
compilerOptions,
host,
)
candidates = append(candidates, specPair{
ending: ending,
value: result,
})
}
if len(tspath.TryGetExtensionFromPath(pattern)) > 0 {
candidates = append(candidates, specPair{
ending: ModuleSpecifierEndingJsExtension,
value: relativeToBaseUrl,
})
}
if indexOfStar != -1 {
prefix := pattern[0:indexOfStar]
suffix := pattern[indexOfStar+1:]
for _, c := range candidates {
value := c.value
if len(value) >= len(prefix)+len(suffix) &&
stringutil.HasPrefix(value, prefix, caseSensitive) && // TODO: possible strada bug: these are not case-switched in strada
stringutil.HasSuffix(value, suffix, caseSensitive) &&
validateEnding(c, relativeToBaseUrl, compilerOptions, host) {
matchedStar := value[len(prefix) : len(value)-len(suffix)]
if !tspath.PathIsRelative(matchedStar) {
return replaceFirstStar(key, matchedStar)
}
}
}
} else if core.Some(candidates, func(c specPair) bool { return c.ending != ModuleSpecifierEndingMinimal && pattern == c.value }) ||
core.Some(candidates, func(c specPair) bool {
return c.ending == ModuleSpecifierEndingMinimal && pattern == c.value && validateEnding(c, relativeToBaseUrl, compilerOptions, host)
}) {
return key
}
}
}
return ""
}
func validateEnding(c specPair, relativeToBaseUrl string, compilerOptions *core.CompilerOptions, host ModuleSpecifierGenerationHost) bool {
// Optimization: `removeExtensionAndIndexPostFix` can query the file system (a good bit) if `ending` is `Minimal`, the basename
// is 'index', and a `host` is provided. To avoid that until it's unavoidable, we ran the function with no `host` above. Only
// here, after we've checked that the minimal ending is indeed a match (via the length and prefix/suffix checks / `some` calls),
// do we check that the host-validated result is consistent with the answer we got before. If it's not, it falls back to the
// `ModuleSpecifierEnding.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is
// that every module resolution mode that supports dropping extensions also supports dropping `/index`. Like literally
// everything else in this file, this logic needs to be updated if that's not true in some future module resolution mode.)
return c.ending != ModuleSpecifierEndingMinimal || c.value == processEnding(relativeToBaseUrl, []ModuleSpecifierEnding{c.ending}, compilerOptions, host)
}
func tryGetModuleNameFromExportsOrImports(
options *core.CompilerOptions,
host ModuleSpecifierGenerationHost,
targetFilePath string,
packageDirectory string,
packageName string,
exports packagejson.ExportsOrImports,
conditions []string,
mode MatchingMode,
isImports bool,
preferTsExtension bool,
) string {
switch exports.Type {
case packagejson.JSONValueTypeNotPresent:
return ""
case packagejson.JSONValueTypeString:
strValue := exports.Value.(string)
// possible strada bug? Always uses compilerOptions of the host project, not those applicable to the targeted package.json!
var outputFile string
var declarationFile string
if isImports {
outputFile = outputpaths.GetOutputJSFileNameWorker(targetFilePath, options, host)
declarationFile = outputpaths.GetOutputDeclarationFileNameWorker(targetFilePath, options, host)
}
pathOrPattern := tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(packageDirectory, strValue), "")
var extensionSwappedTarget string
if tspath.HasTSFileExtension(targetFilePath) {
extensionSwappedTarget = tspath.RemoveFileExtension(targetFilePath) + tryGetJSExtensionForFile(targetFilePath, options)
}
canTryTsExtension := preferTsExtension && tspath.HasImplementationTSFileExtension(targetFilePath)
compareOpts := tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
}
switch mode {
case MatchingModeExact:
if len(extensionSwappedTarget) > 0 && tspath.ComparePaths(extensionSwappedTarget, pathOrPattern, compareOpts) == 0 ||
tspath.ComparePaths(targetFilePath, pathOrPattern, compareOpts) == 0 ||
len(outputFile) > 0 && tspath.ComparePaths(outputFile, pathOrPattern, compareOpts) == 0 ||
len(declarationFile) > 0 && tspath.ComparePaths(declarationFile, pathOrPattern, compareOpts) == 0 {
return packageName
}
case MatchingModeDirectory:
if canTryTsExtension && tspath.ContainsPath(targetFilePath, pathOrPattern, compareOpts) {
fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, targetFilePath, compareOpts)
return tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(tspath.CombinePaths(packageName, strValue), fragment), "")
}
if len(extensionSwappedTarget) > 0 && tspath.ContainsPath(pathOrPattern, extensionSwappedTarget, compareOpts) {
fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, extensionSwappedTarget, compareOpts)
return tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(tspath.CombinePaths(packageName, strValue), fragment), "")
}
if !canTryTsExtension && tspath.ContainsPath(pathOrPattern, targetFilePath, compareOpts) {
fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, targetFilePath, compareOpts)
return tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(tspath.CombinePaths(packageName, strValue), fragment), "")
}
if len(outputFile) > 0 && tspath.ContainsPath(pathOrPattern, outputFile, compareOpts) {
fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, outputFile, compareOpts)
return tspath.CombinePaths(packageName, fragment)
}
if len(declarationFile) > 0 && tspath.ContainsPath(pathOrPattern, declarationFile, compareOpts) {
fragment := tspath.GetRelativePathFromDirectory(pathOrPattern, declarationFile, compareOpts)
jsExtension := getJSExtensionForFile(declarationFile, options)
fragmentWithJsExtension := tspath.ChangeExtension(fragment, jsExtension)
return tspath.CombinePaths(packageName, fragmentWithJsExtension)
}
case MatchingModePattern:
starPos := strings.Index(pathOrPattern, "*")
leadingSlice := pathOrPattern[0:starPos]
trailingSlice := pathOrPattern[starPos+1:]
caseSensitive := host.UseCaseSensitiveFileNames()
if canTryTsExtension && stringutil.HasPrefix(targetFilePath, leadingSlice, caseSensitive) && stringutil.HasSuffix(targetFilePath, trailingSlice, caseSensitive) {
starReplacement := targetFilePath[len(leadingSlice) : len(targetFilePath)-len(trailingSlice)]
return replaceFirstStar(packageName, starReplacement)
}
if len(extensionSwappedTarget) > 0 && stringutil.HasPrefix(extensionSwappedTarget, leadingSlice, caseSensitive) && stringutil.HasSuffix(extensionSwappedTarget, trailingSlice, caseSensitive) {
starReplacement := extensionSwappedTarget[len(leadingSlice) : len(extensionSwappedTarget)-len(trailingSlice)]
return replaceFirstStar(packageName, starReplacement)
}
if !canTryTsExtension && stringutil.HasPrefix(targetFilePath, leadingSlice, caseSensitive) && stringutil.HasSuffix(targetFilePath, trailingSlice, caseSensitive) {
starReplacement := targetFilePath[len(leadingSlice) : len(targetFilePath)-len(trailingSlice)]
return replaceFirstStar(packageName, starReplacement)
}
if len(outputFile) > 0 && stringutil.HasPrefix(outputFile, leadingSlice, caseSensitive) && stringutil.HasSuffix(outputFile, trailingSlice, caseSensitive) {
starReplacement := outputFile[len(leadingSlice) : len(outputFile)-len(trailingSlice)]
return replaceFirstStar(packageName, starReplacement)
}
if len(declarationFile) > 0 && stringutil.HasPrefix(declarationFile, leadingSlice, caseSensitive) && stringutil.HasSuffix(declarationFile, trailingSlice, caseSensitive) {
starReplacement := declarationFile[len(leadingSlice) : len(declarationFile)-len(trailingSlice)]
substituted := replaceFirstStar(packageName, starReplacement)
jsExtension := tryGetJSExtensionForFile(declarationFile, options)
if len(jsExtension) > 0 {
return tspath.ChangeFullExtension(substituted, jsExtension)
}
}
}
return ""
case packagejson.JSONValueTypeArray:
arr := exports.AsArray()
for _, e := range arr {
result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, e, conditions, mode, isImports, preferTsExtension)
if len(result) > 0 {
return result
}
}
case packagejson.JSONValueTypeObject:
// conditional mapping
obj := exports.AsObject()
for key, value := range obj.Entries() {
if key == "default" || slices.Contains(conditions, key) || isApplicableVersionedTypesKey(conditions, key) {
result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, value, conditions, mode, isImports, preferTsExtension)
if len(result) > 0 {
return result
}
}
}
case packagejson.JSONValueTypeNull:
return ""
}
return ""
}
// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`?
// Because when this is called by the declaration emitter, `importingSourceFile` is the implementation
// file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the
// one currently being produced; the latter to the one being imported). We need an implementation file
// just to get its `impliedNodeFormat` and to detect certain preferences from existing import module
// specifiers.
func GetModuleSpecifier(
compilerOptions *core.CompilerOptions,
host ModuleSpecifierGenerationHost,
importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile
importingSourceFileName string,
oldImportSpecifier string, // used only in updatingModuleSpecifier
toFileName string,
options ModuleSpecifierOptions,
) string {
userPreferences := UserPreferences{}
info := getInfo(importingSourceFileName, host)
modulePaths := getAllModulePaths(info, toFileName, host, compilerOptions, userPreferences, options)
preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, oldImportSpecifier)
resolutionMode := options.OverrideImportMode
if resolutionMode == core.ResolutionModeNone {
resolutionMode = host.GetDefaultResolutionModeForFile(importingSourceFile)
}
for _, modulePath := range modulePaths {
if firstDefined := tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, false /*packageNameOnly*/, options.OverrideImportMode); len(firstDefined) > 0 {
return firstDefined
} else if firstDefined := getLocalModuleSpecifier(toFileName, info, compilerOptions, host, resolutionMode, preferences, false); len(firstDefined) > 0 {
return firstDefined
}
}
return ""
}