1287 lines
52 KiB
Go
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 ""
|
|
}
|