package modulespecifiers import ( "fmt" "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/module" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/packagejson" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/semver" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "github.com/dlclark/regexp2" ) func isNonGlobalAmbientModule(node *ast.Node) bool { return ast.IsModuleDeclaration(node) && ast.IsStringLiteral(node.Name()) } func comparePathsByRedirectAndNumberOfDirectorySeparators(a ModulePath, b ModulePath) int { if a.IsRedirect == b.IsRedirect { return strings.Count(a.FileName, "/") - strings.Count(b.FileName, "/") } if a.IsRedirect { return 1 } return -1 } func PathIsBareSpecifier(path string) bool { return !tspath.PathIsAbsolute(path) && !tspath.PathIsRelative(path) } func isExcludedByRegex(moduleSpecifier string, excludes []string) bool { for _, pattern := range excludes { compiled, err := regexp2.Compile(pattern, regexp2.None) if err != nil { continue } match, _ := compiled.MatchString(moduleSpecifier) if match { return true } } return false } /** * 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)] } } func getJSExtensionForFile(fileName string, options *core.CompilerOptions) string { result := 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 tryGetJSExtensionForFile(fileName string, options *core.CompilerOptions) string { ext := tspath.TryGetExtensionFromPath(fileName) switch ext { case tspath.ExtensionTs, tspath.ExtensionDts: return tspath.ExtensionJs case tspath.ExtensionTsx: if options.Jsx == core.JsxEmitPreserve { return tspath.ExtensionJsx } return tspath.ExtensionJs case tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionJson: return ext case tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionMjs: return tspath.ExtensionMjs case tspath.ExtensionDcts, tspath.ExtensionCts, tspath.ExtensionCjs: return tspath.ExtensionCjs default: return "" } } 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 len(relativePath) > 0 && 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 } var typeScriptVersion = semver.MustParse(core.Version()) // TODO: unify with clone inside module resolver? func isApplicableVersionedTypesKey(conditions []string, key string) bool { if !slices.Contains(conditions, "types") { return false // only apply versioned types conditions if the types condition is applied } if !strings.HasPrefix(key, "types@") { return false } range_, ok := semver.TryParseVersionRange(key[len("types@"):]) if !ok { return false } return range_.Test(&typeScriptVersion) } 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 GetPackageNameFromTypesPackageName(mangledName string) string { withoutAtTypePrefix := strings.TrimPrefix(mangledName, "@types/") if withoutAtTypePrefix != mangledName { return module.UnmangleScopedPackageName(withoutAtTypePrefix) } return mangledName } func allKeysStartWithDot(obj *collections.OrderedMap[string, packagejson.ExportsOrImports]) bool { for k := range obj.Keys() { if !strings.HasPrefix(k, ".") { return false } } return true }