503 lines
15 KiB
Go
503 lines
15 KiB
Go
package modulespecifiers
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/module"
|
|
"github.com/microsoft/typescript-go/internal/packagejson"
|
|
"github.com/microsoft/typescript-go/internal/tsoptions"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
)
|
|
|
|
type regexPatternCacheKey struct {
|
|
pattern string
|
|
caseInsensitive bool
|
|
}
|
|
|
|
var (
|
|
regexPatternCacheMu sync.RWMutex
|
|
regexPatternCache = make(map[regexPatternCacheKey]*regexp.Regexp)
|
|
)
|
|
|
|
func comparePathsByRedirect(a ModulePath, b ModulePath, useCaseSensitiveFileNames bool) int {
|
|
// Redirects sort first, matching Strada's compareBooleans(b.isRedirect, a.isRedirect).
|
|
if c := core.CompareBooleans(b.IsRedirect, a.IsRedirect); c != 0 {
|
|
return c
|
|
}
|
|
if c := tspath.CompareNumberOfDirectorySeparators(a.FileName, b.FileName); c != 0 {
|
|
return c
|
|
}
|
|
// Strada relies on Map insertion order to break remaining ties deterministically;
|
|
// Go maps are unordered, so compare paths to keep the ordering stable.
|
|
return tspath.ComparePaths(a.FileName, b.FileName, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: useCaseSensitiveFileNames})
|
|
}
|
|
|
|
func PathIsBareSpecifier(path string) bool {
|
|
return !tspath.PathIsAbsolute(path) && !tspath.PathIsRelative(path)
|
|
}
|
|
|
|
func IsExcludedByRegex(moduleSpecifier string, excludes []string) bool {
|
|
for _, pattern := range excludes {
|
|
re := stringToRegex(pattern)
|
|
if re == nil {
|
|
continue
|
|
}
|
|
if re.MatchString(moduleSpecifier) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func stringToRegex(pattern string) *regexp.Regexp {
|
|
caseInsensitive := false
|
|
|
|
if len(pattern) > 2 && pattern[0] == '/' {
|
|
lastSlash := strings.LastIndex(pattern, "/")
|
|
if lastSlash > 0 {
|
|
hasUnescapedMiddleSlash := false
|
|
for i := 1; i < lastSlash; i++ {
|
|
if pattern[i] == '/' && (i == 0 || pattern[i-1] != '\\') {
|
|
hasUnescapedMiddleSlash = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasUnescapedMiddleSlash {
|
|
flags := pattern[lastSlash+1:]
|
|
pattern = pattern[1:lastSlash]
|
|
|
|
for _, flag := range flags {
|
|
switch flag {
|
|
case 'i':
|
|
caseInsensitive = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
key := regexPatternCacheKey{pattern, caseInsensitive}
|
|
|
|
regexPatternCacheMu.RLock()
|
|
re, ok := regexPatternCache[key]
|
|
regexPatternCacheMu.RUnlock()
|
|
if ok {
|
|
return re
|
|
}
|
|
|
|
regexPatternCacheMu.Lock()
|
|
defer regexPatternCacheMu.Unlock()
|
|
|
|
re, ok = regexPatternCache[key]
|
|
if ok {
|
|
return re
|
|
}
|
|
|
|
if len(regexPatternCache) > 1000 {
|
|
clear(regexPatternCache)
|
|
}
|
|
|
|
pattern = strings.Clone(pattern)
|
|
key.pattern = pattern
|
|
|
|
compilePattern := pattern
|
|
if caseInsensitive {
|
|
compilePattern = "(?i:" + pattern + ")"
|
|
}
|
|
|
|
compiled, err := regexp.Compile(compilePattern)
|
|
if err != nil {
|
|
regexPatternCache[key] = nil
|
|
return nil
|
|
}
|
|
regexPatternCache[key] = compiled
|
|
return compiled
|
|
}
|
|
|
|
/**
|
|
* Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
|
|
* with `./` or `../`) so as not to be confused with an unprefixed module name.
|
|
*
|
|
* ```ts
|
|
* ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext"
|
|
* ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext"
|
|
* ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext"
|
|
* ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext"
|
|
* ```
|
|
*
|
|
*/
|
|
func ensurePathIsNonModuleName(path string) string {
|
|
if PathIsBareSpecifier(path) {
|
|
return "./" + path
|
|
}
|
|
return path
|
|
}
|
|
|
|
func GetJSExtensionForDeclarationFileExtension(ext string) string {
|
|
switch ext {
|
|
case tspath.ExtensionDts:
|
|
return tspath.ExtensionJs
|
|
case tspath.ExtensionDmts:
|
|
return tspath.ExtensionMjs
|
|
case tspath.ExtensionDcts:
|
|
return tspath.ExtensionCjs
|
|
default:
|
|
// .d.json.ts and the like
|
|
return ext[len(".d") : len(ext)-len(tspath.ExtensionTs)]
|
|
}
|
|
}
|
|
|
|
// TryGetRealFileNameForNonJSDeclarationFileName remaps files like `foo.d.json.ts` or
|
|
// `foo.module.d.css.ts` back to their real non-JS names.
|
|
func TryGetRealFileNameForNonJSDeclarationFileName(fileName string) string {
|
|
baseName := tspath.GetBaseFileName(fileName)
|
|
// Ends with .ts, contains ".d.", and is NOT a standard .d.ts file
|
|
if !strings.HasSuffix(fileName, tspath.ExtensionTs) ||
|
|
!strings.Contains(baseName, ".d.") ||
|
|
strings.HasSuffix(baseName, tspath.ExtensionDts) {
|
|
return ""
|
|
}
|
|
noExtension := tspath.RemoveExtension(fileName, tspath.ExtensionTs)
|
|
lastDotIndex := strings.LastIndex(noExtension, ".")
|
|
ext := noExtension[lastDotIndex:]
|
|
before, _, _ := strings.Cut(noExtension, ".d.")
|
|
return before + ext
|
|
}
|
|
|
|
func getJSExtensionForFile(fileName string, options *core.CompilerOptions) string {
|
|
result := module.TryGetJSExtensionForFile(fileName, options)
|
|
if len(result) == 0 {
|
|
panic(fmt.Sprintf("Extension %s is unsupported:: FileName:: %s", extensionFromPath(fileName), fileName))
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Gets the extension from a path.
|
|
* Path must have a valid extension.
|
|
*/
|
|
func extensionFromPath(path string) string {
|
|
ext := tspath.TryGetExtensionFromPath(path)
|
|
if len(ext) == 0 {
|
|
panic(fmt.Sprintf("File %s has unknown extension.", path))
|
|
}
|
|
return ext
|
|
}
|
|
|
|
func tryGetAnyFileFromPath(host ModuleSpecifierGenerationHost, path string) bool {
|
|
// !!! TODO: shouldn't this use readdir instead of fileexists for perf?
|
|
// We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
|
|
extGroups := tsoptions.GetSupportedExtensions(
|
|
&core.CompilerOptions{
|
|
AllowJs: core.TSTrue,
|
|
},
|
|
[]tsoptions.FileExtensionInfo{
|
|
{
|
|
Extension: "node",
|
|
IsMixedContent: false,
|
|
ScriptKind: core.ScriptKindExternal,
|
|
},
|
|
{
|
|
Extension: "json",
|
|
IsMixedContent: false,
|
|
ScriptKind: core.ScriptKindJSON,
|
|
},
|
|
},
|
|
)
|
|
for _, exts := range extGroups {
|
|
for _, e := range exts {
|
|
fullPath := path + e
|
|
if host.FileExists(tspath.GetNormalizedAbsolutePath(fullPath, host.GetCurrentDirectory())) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getPathsRelativeToRootDirs(path string, rootDirs []string, useCaseSensitiveFileNames bool) []string {
|
|
var results []string
|
|
for _, rootDir := range rootDirs {
|
|
relativePath := getRelativePathIfInSameVolume(path, rootDir, useCaseSensitiveFileNames)
|
|
if !isPathRelativeToParent(relativePath) {
|
|
results = append(results, relativePath)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
func isPathRelativeToParent(path string) bool {
|
|
return strings.HasPrefix(path, "..")
|
|
}
|
|
|
|
func getRelativePathIfInSameVolume(path string, directoryPath string, useCaseSensitiveFileNames bool) string {
|
|
relativePath := tspath.GetRelativePathToDirectoryOrUrl(directoryPath, path, false, tspath.ComparePathsOptions{
|
|
UseCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
|
CurrentDirectory: directoryPath,
|
|
})
|
|
if tspath.IsRootedDiskPath(relativePath) {
|
|
return ""
|
|
}
|
|
return relativePath
|
|
}
|
|
|
|
func packageJsonPathsAreEqual(a string, b string, options tspath.ComparePathsOptions) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
if len(a) == 0 || len(b) == 0 {
|
|
return false
|
|
}
|
|
return tspath.ComparePaths(a, b, options) == 0
|
|
}
|
|
|
|
func prefersTsExtension(allowedEndings []ModuleSpecifierEnding) bool {
|
|
jsPriority := slices.Index(allowedEndings, ModuleSpecifierEndingJsExtension)
|
|
tsPriority := slices.Index(allowedEndings, ModuleSpecifierEndingTsExtension)
|
|
if tsPriority > -1 {
|
|
return tsPriority < jsPriority
|
|
}
|
|
return false
|
|
}
|
|
|
|
func replaceFirstStar(s string, replacement string) string {
|
|
return strings.Replace(s, "*", replacement, 1)
|
|
}
|
|
|
|
type NodeModulePathParts struct {
|
|
TopLevelNodeModulesIndex int
|
|
TopLevelPackageNameIndex int
|
|
PackageRootIndex int
|
|
FileNameIndex int
|
|
}
|
|
|
|
type nodeModulesPathParseState uint8
|
|
|
|
const (
|
|
nodeModulesPathParseStateBeforeNodeModules nodeModulesPathParseState = iota
|
|
nodeModulesPathParseStateNodeModules
|
|
nodeModulesPathParseStateScope
|
|
nodeModulesPathParseStatePackageContent
|
|
)
|
|
|
|
func GetNodeModulePathParts(fullPath string) *NodeModulePathParts {
|
|
// If fullPath can't be valid module file within node_modules, returns undefined.
|
|
// Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js
|
|
// Returns indices: ^ ^ ^ ^
|
|
|
|
topLevelNodeModulesIndex := 0
|
|
topLevelPackageNameIndex := 0
|
|
packageRootIndex := 0
|
|
fileNameIndex := 0
|
|
|
|
partStart := 0
|
|
partEnd := 0
|
|
state := nodeModulesPathParseStateBeforeNodeModules
|
|
|
|
for partEnd >= 0 {
|
|
partStart = partEnd
|
|
partEnd = core.IndexAfter(fullPath, "/", partStart+1)
|
|
switch state {
|
|
case nodeModulesPathParseStateBeforeNodeModules:
|
|
if strings.Index(fullPath[partStart:], "/node_modules/") == 0 {
|
|
topLevelNodeModulesIndex = partStart
|
|
topLevelPackageNameIndex = partEnd
|
|
state = nodeModulesPathParseStateNodeModules
|
|
}
|
|
case nodeModulesPathParseStateNodeModules, nodeModulesPathParseStateScope:
|
|
if state == nodeModulesPathParseStateNodeModules && fullPath[partStart+1] == '@' {
|
|
state = nodeModulesPathParseStateScope
|
|
} else {
|
|
packageRootIndex = partEnd
|
|
state = nodeModulesPathParseStatePackageContent
|
|
}
|
|
case nodeModulesPathParseStatePackageContent:
|
|
if strings.Index(fullPath[partStart:], "/node_modules/") == 0 {
|
|
state = nodeModulesPathParseStateNodeModules
|
|
} else {
|
|
state = nodeModulesPathParseStatePackageContent
|
|
}
|
|
}
|
|
}
|
|
|
|
fileNameIndex = partStart
|
|
|
|
if state > nodeModulesPathParseStateNodeModules {
|
|
return &NodeModulePathParts{
|
|
TopLevelNodeModulesIndex: topLevelNodeModulesIndex,
|
|
TopLevelPackageNameIndex: topLevelPackageNameIndex,
|
|
PackageRootIndex: packageRootIndex,
|
|
FileNameIndex: fileNameIndex,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetNodeModulesPackageName(
|
|
compilerOptions *core.CompilerOptions,
|
|
importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile
|
|
nodeModulesFileName string,
|
|
host ModuleSpecifierGenerationHost,
|
|
preferences UserPreferences,
|
|
options ModuleSpecifierOptions,
|
|
) string {
|
|
info := getInfo(importingSourceFile.FileName(), host)
|
|
modulePaths := getAllModulePaths(info, nodeModulesFileName, host, compilerOptions, preferences, options)
|
|
for _, modulePath := range modulePaths {
|
|
if result := tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, preferences, true /*packageNameOnly*/, options.OverrideImportMode); len(result) > 0 {
|
|
return result
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func allKeysStartWithDot(obj *collections.OrderedMap[string, packagejson.ExportsOrImports]) bool {
|
|
for k := range obj.Keys() {
|
|
if !strings.HasPrefix(k, ".") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func GetPackageNameFromDirectory(fileOrDirectoryPath string) string {
|
|
idx := strings.LastIndex(fileOrDirectoryPath, "/node_modules/")
|
|
if idx == -1 {
|
|
return ""
|
|
}
|
|
|
|
basename := fileOrDirectoryPath[idx+len("/node_modules/"):]
|
|
if basename[0] == '.' {
|
|
return ""
|
|
}
|
|
|
|
nextSlash := strings.Index(basename, "/")
|
|
if nextSlash == -1 {
|
|
return basename
|
|
}
|
|
|
|
if basename[0] != '@' || nextSlash == len(basename)-1 {
|
|
return basename[:nextSlash]
|
|
}
|
|
|
|
secondSlash := strings.Index(basename[nextSlash+1:], "/")
|
|
if secondSlash == -1 {
|
|
return basename
|
|
}
|
|
|
|
return basename[:nextSlash+1+secondSlash]
|
|
}
|
|
|
|
// ProcessEntrypointEnding processes a pre-computed module specifier from a package.json exports
|
|
// entrypoint according to the entrypoint's Ending type and the user's preferred endings.
|
|
func ProcessEntrypointEnding(
|
|
entrypoint *module.ResolvedEntrypoint,
|
|
prefs UserPreferences,
|
|
host ModuleSpecifierGenerationHost,
|
|
options *core.CompilerOptions,
|
|
importingSourceFile SourceFileForSpecifierGeneration,
|
|
allowedEndings []ModuleSpecifierEnding,
|
|
) string {
|
|
specifier := entrypoint.ModuleSpecifier
|
|
if entrypoint.Ending == module.EndingFixed {
|
|
return specifier
|
|
}
|
|
|
|
if len(allowedEndings) == 0 {
|
|
allowedEndings = GetAllowedEndingsInPreferredOrder(
|
|
prefs,
|
|
host,
|
|
options,
|
|
importingSourceFile,
|
|
"",
|
|
host.GetDefaultResolutionModeForFile(importingSourceFile),
|
|
)
|
|
}
|
|
|
|
preferredEnding := allowedEndings[0]
|
|
|
|
// Handle declaration file extensions
|
|
dtsExtension := tspath.GetDeclarationFileExtension(specifier)
|
|
if dtsExtension != "" {
|
|
switch preferredEnding {
|
|
case ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension:
|
|
// Map .d.ts -> .js, .d.mts -> .mjs, .d.cts -> .cjs
|
|
jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension)
|
|
return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false)
|
|
case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex:
|
|
if entrypoint.Ending == module.EndingChangeable {
|
|
// .d.mts/.d.cts must keep an extension; rewrite to .mjs/.cjs instead of dropping
|
|
if dtsExtension == tspath.ExtensionDts {
|
|
specifier = tspath.RemoveExtension(specifier, dtsExtension)
|
|
if preferredEnding == ModuleSpecifierEndingMinimal {
|
|
specifier = strings.TrimSuffix(specifier, "/index")
|
|
}
|
|
return specifier
|
|
}
|
|
jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension)
|
|
return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false)
|
|
}
|
|
// EndingExtensionChangeable - can only change extension, not remove it
|
|
jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension)
|
|
return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false)
|
|
}
|
|
return specifier
|
|
}
|
|
|
|
// Handle .ts/.tsx/.mts/.cts extensions
|
|
if tspath.FileExtensionIsOneOf(specifier, []string{tspath.ExtensionTs, tspath.ExtensionTsx, tspath.ExtensionMts, tspath.ExtensionCts}) {
|
|
switch preferredEnding {
|
|
case ModuleSpecifierEndingTsExtension:
|
|
return specifier
|
|
case ModuleSpecifierEndingJsExtension:
|
|
if jsExtension := module.TryGetJSExtensionForFile(specifier, options); jsExtension != "" {
|
|
return tspath.RemoveFileExtension(specifier) + jsExtension
|
|
}
|
|
return specifier
|
|
case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex:
|
|
if entrypoint.Ending == module.EndingChangeable {
|
|
specifier = tspath.RemoveFileExtension(specifier)
|
|
if preferredEnding == ModuleSpecifierEndingMinimal {
|
|
specifier = strings.TrimSuffix(specifier, "/index")
|
|
}
|
|
return specifier
|
|
}
|
|
// EndingExtensionChangeable - can only change extension, not remove it
|
|
if jsExtension := module.TryGetJSExtensionForFile(specifier, options); jsExtension != "" {
|
|
return tspath.RemoveFileExtension(specifier) + jsExtension
|
|
}
|
|
return specifier
|
|
}
|
|
return specifier
|
|
}
|
|
|
|
// Handle .js/.jsx/.mjs/.cjs extensions
|
|
if tspath.FileExtensionIsOneOf(specifier, []string{tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionMjs, tspath.ExtensionCjs}) {
|
|
switch preferredEnding {
|
|
case ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension:
|
|
return specifier
|
|
case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex:
|
|
if entrypoint.Ending == module.EndingChangeable {
|
|
specifier = tspath.RemoveFileExtension(specifier)
|
|
if preferredEnding == ModuleSpecifierEndingMinimal {
|
|
specifier = strings.TrimSuffix(specifier, "/index")
|
|
}
|
|
return specifier
|
|
}
|
|
// EndingExtensionChangeable - keep the extension
|
|
return specifier
|
|
}
|
|
return specifier
|
|
}
|
|
|
|
// For other extensions (like .json), return as-is
|
|
return specifier
|
|
}
|