324 lines
11 KiB
Go
324 lines
11 KiB
Go
package autoimport
|
|
|
|
import (
|
|
"context"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/checker"
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/compiler"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/module"
|
|
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
|
"github.com/microsoft/typescript-go/internal/packagejson"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
"github.com/microsoft/typescript-go/internal/vfs"
|
|
"github.com/microsoft/typescript-go/internal/vfs/wrapvfs"
|
|
)
|
|
|
|
func tryGetModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string, bool) {
|
|
if !symbol.IsExternalModule() {
|
|
return "", "", false
|
|
}
|
|
decl := ast.GetNonAugmentationDeclaration(symbol)
|
|
if decl == nil {
|
|
return "", "", false
|
|
}
|
|
if decl.Kind == ast.KindSourceFile {
|
|
return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName(), true
|
|
}
|
|
if ast.IsModuleWithStringLiteralName(decl) {
|
|
return ModuleID(decl.Name().Text()), "", true
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
func getModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string) {
|
|
if !symbol.IsExternalModule() {
|
|
panic("symbol is not an external module")
|
|
}
|
|
decl := ast.GetNonAugmentationDeclaration(symbol)
|
|
if decl == nil {
|
|
panic("module symbol has no non-augmentation declaration")
|
|
}
|
|
if decl.Kind == ast.KindSourceFile {
|
|
return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName()
|
|
}
|
|
if ast.IsModuleWithStringLiteralName(decl) {
|
|
return ModuleID(decl.Name().Text()), ""
|
|
}
|
|
panic("could not determine module ID of module symbol")
|
|
}
|
|
|
|
// wordIndices splits an identifier into its constituent words based on camelCase and snake_case conventions
|
|
// by returning the starting byte indices of each word. The first index is always 0.
|
|
// - CamelCase
|
|
// ^ ^
|
|
// - snake_case
|
|
// ^ ^
|
|
// - ParseURL
|
|
// ^ ^
|
|
// - __proto__
|
|
// ^
|
|
func wordIndices(s string) []int {
|
|
var indices []int
|
|
for byteIndex, runeValue := range s {
|
|
if byteIndex == 0 {
|
|
indices = append(indices, byteIndex)
|
|
continue
|
|
}
|
|
if runeValue == '_' {
|
|
if byteIndex+1 < len(s) && s[byteIndex+1] != '_' {
|
|
indices = append(indices, byteIndex+1)
|
|
}
|
|
continue
|
|
}
|
|
if unicode.IsUpper(runeValue) && (unicode.IsLower(core.FirstResult(utf8.DecodeLastRuneInString(s[:byteIndex]))) || (byteIndex+1 < len(s) && unicode.IsLower(core.FirstResult(utf8.DecodeRuneInString(s[byteIndex+1:]))))) {
|
|
indices = append(indices, byteIndex)
|
|
}
|
|
}
|
|
return indices
|
|
}
|
|
|
|
func getPackageNamesInNodeModules(nodeModulesDir string, fs vfs.FS) *collections.Set[string] {
|
|
packageNames := &collections.Set[string]{}
|
|
if tspath.GetBaseFileName(nodeModulesDir) != "node_modules" {
|
|
panic("nodeModulesDir is not a node_modules directory")
|
|
}
|
|
// A missing node_modules directory yields no entries (GetAccessibleEntries returns
|
|
// empty), so there's no need to check existence first: a deleted node_modules is
|
|
// handled upstream in updateBucketAndDirectoryExistence, which drops the bucket.
|
|
entries := fs.GetAccessibleEntries(nodeModulesDir)
|
|
for _, baseName := range entries.Directories {
|
|
if baseName[0] == '.' {
|
|
continue
|
|
}
|
|
if baseName[0] == '@' {
|
|
scopedDirPath := tspath.CombinePaths(nodeModulesDir, baseName)
|
|
for _, scopedPackageDirName := range fs.GetAccessibleEntries(scopedDirPath).Directories {
|
|
scopedBaseName := tspath.GetBaseFileName(scopedPackageDirName)
|
|
if baseName == "@types" {
|
|
packageNames.Add(module.GetPackageNameFromTypesPackageName(tspath.CombinePaths("@types", scopedBaseName)))
|
|
} else {
|
|
packageNames.Add(tspath.CombinePaths(baseName, scopedBaseName))
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
packageNames.Add(baseName)
|
|
}
|
|
return packageNames
|
|
}
|
|
|
|
func getDefaultLikeExportNameFromDeclaration(symbol *ast.Symbol) string {
|
|
for _, d := range symbol.Declarations {
|
|
// "export default" in this case. See `ExportAssignment`for more details.
|
|
if ast.IsExportAssignment(d) {
|
|
if innerExpression := ast.SkipOuterExpressions(d.Expression(), ast.OEKAll); ast.IsIdentifier(innerExpression) {
|
|
return innerExpression.Text()
|
|
}
|
|
continue
|
|
}
|
|
// "export { ~ as default }"
|
|
if ast.IsExportSpecifier(d) && d.Symbol().Flags == ast.SymbolFlagsAlias && d.PropertyName() != nil {
|
|
if d.PropertyName().Kind == ast.KindIdentifier {
|
|
return d.PropertyName().Text()
|
|
}
|
|
continue
|
|
}
|
|
// GH#52694
|
|
if name := ast.GetNameOfDeclaration(d); name != nil && name.Kind == ast.KindIdentifier {
|
|
return name.Text()
|
|
}
|
|
if symbol.Parent != nil && !checker.IsExternalModuleSymbol(symbol.Parent) {
|
|
return symbol.Parent.Name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getResolvedPackageNames(ctx context.Context, program *compiler.Program) *collections.Set[string] {
|
|
rawNames := program.ResolvedPackageNames()
|
|
unresolvedPackageNames := program.UnresolvedPackageNames()
|
|
|
|
// Normalize @types/ package names to their actual package names
|
|
// (e.g., "@types/react" → "react"). ResolvedPackageNames can contain
|
|
// @types names when the program resolves an import like "react" to
|
|
// "@types/react/index.d.ts" via the PackageId.Name field.
|
|
resolvedPackageNames := collections.NewSetWithSizeHint[string](rawNames.Len())
|
|
for name := range rawNames.Keys() {
|
|
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
|
|
}
|
|
|
|
for _, name := range program.Options().Types {
|
|
if name != "*" {
|
|
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
|
|
}
|
|
}
|
|
|
|
if unresolvedPackageNames.Len() > 0 {
|
|
checker, done := program.GetTypeChecker(ctx)
|
|
defer done()
|
|
for name := range unresolvedPackageNames.Keys() {
|
|
if symbol := checker.TryFindAmbientModule(name); symbol != nil {
|
|
declaringFile := ast.GetSourceFileOfModule(symbol)
|
|
if packageName := modulespecifiers.GetPackageNameFromDirectory(declaringFile.FileName()); packageName != "" {
|
|
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(packageName))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return resolvedPackageNames
|
|
}
|
|
|
|
// addProjectReferenceOutputMappings adds output .d.ts to source file mappings
|
|
// from a program's project references to the provided map.
|
|
// This is used during node_modules bucket building to redirect extraction
|
|
// from output files to source files when the output is from a project reference.
|
|
func addProjectReferenceOutputMappings(program *compiler.Program, result map[tspath.Path]string) {
|
|
refs := program.GetResolvedProjectReferences()
|
|
for _, ref := range refs {
|
|
if ref == nil {
|
|
continue
|
|
}
|
|
ref.ParseInputOutputNames()
|
|
for outputDtsPath, mapping := range ref.OutputDtsToProjectReference() {
|
|
// Only add if not already present (first program wins)
|
|
if _, exists := result[outputDtsPath]; !exists {
|
|
result[outputDtsPath] = mapping.Source
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func createCheckerPool(program checker.Program) (getChecker func() (*checker.Checker, func()), closePool func(), getCreatedCount func() int32) {
|
|
maxSize := int32(runtime.GOMAXPROCS(0))
|
|
pool := make(chan *checker.Checker, maxSize)
|
|
var created atomic.Int32
|
|
|
|
return func() (*checker.Checker, func()) {
|
|
// Try to get an existing checker
|
|
select {
|
|
case ch := <-pool:
|
|
return ch, func() { pool <- ch }
|
|
default:
|
|
break
|
|
}
|
|
// Try to create a new one if under limit
|
|
for {
|
|
current := created.Load()
|
|
if current >= maxSize {
|
|
// At limit, wait for one to become available
|
|
ch := <-pool
|
|
return ch, func() { pool <- ch }
|
|
}
|
|
if created.CompareAndSwap(current, current+1) {
|
|
ch := core.FirstResult(checker.NewChecker(program, nil))
|
|
return ch, func() { pool <- ch }
|
|
}
|
|
}
|
|
}, func() {
|
|
close(pool)
|
|
}, func() int32 {
|
|
return created.Load()
|
|
}
|
|
}
|
|
|
|
// addPackageJsonDependencies adds all dependencies and peerDependencies from a package.json
|
|
// to the given set, canonicalizing @types package names to their base names.
|
|
func addPackageJsonDependencies(contents *packagejson.PackageJson, deps *collections.Set[string]) {
|
|
contents.RangeDependencies(func(name, _, field string) bool {
|
|
if name == "" || name == "@types/" || name[0] == '.' {
|
|
// Edge cases that could make us blow up probably
|
|
return true
|
|
}
|
|
if field == "dependencies" || field == "peerDependencies" {
|
|
deps.Add(module.GetPackageNameFromTypesPackageName(name))
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// getPackageRealpathFuncs returns functions to transform between symlink and realpath for files within a package.
|
|
// It calls FS.Realpath once per package directory and uses prefix substitution for files within that directory,
|
|
// avoiding expensive realpath syscalls for each file. For files outside the package (e.g. re-exported
|
|
// dependencies reached through node_modules symlinks), it resolves the file's directory realpath once,
|
|
// finds the symlink boundary (the package root where the symlink lives), and caches that prefix mapping.
|
|
// All subsequent files under the same symlinked package directory use prefix substitution with no syscalls.
|
|
func getPackageRealpathFuncs(fs vfs.FS, packageDir string) (toRealpath, toSymlink func(string) string) {
|
|
realPackageDir := fs.Realpath(packageDir)
|
|
isSymlinked := realPackageDir != packageDir
|
|
// Cache of package-directory-level symlink→realpath prefix mappings for
|
|
// external packages encountered via re-exports. Keyed by the node_modules
|
|
// package directory (e.g. "/app/node_modules/dep"), so all files under
|
|
// that package reuse a single realpath lookup.
|
|
dirCache := make(map[string]string)
|
|
toRealpath = func(fileName string) string {
|
|
// Fast path: files within the package use prefix substitution.
|
|
if isSymlinked {
|
|
if after, ok := strings.CutPrefix(fileName, packageDir); ok {
|
|
return realPackageDir + after
|
|
}
|
|
}
|
|
// Files outside the package (e.g. re-exports into symlinked deps):
|
|
// find the node_modules package directory, resolve it once, and cache.
|
|
pkgDir := module.ParseNodeModuleFromPath(fileName, false /*isFolder*/)
|
|
if pkgDir == "" {
|
|
return fileName
|
|
}
|
|
if realDir, ok := dirCache[pkgDir]; ok {
|
|
if realDir == pkgDir {
|
|
return fileName
|
|
}
|
|
return realDir + fileName[len(pkgDir):]
|
|
}
|
|
realDir := fs.Realpath(pkgDir)
|
|
dirCache[pkgDir] = realDir
|
|
if realDir == pkgDir {
|
|
return fileName
|
|
}
|
|
return realDir + fileName[len(pkgDir):]
|
|
}
|
|
if !isSymlinked {
|
|
return toRealpath, core.Identity
|
|
}
|
|
// toSymlink only handles files within the package directory (reversing the
|
|
// packageDir→realPackageDir substitution). It does not handle arbitrary external
|
|
// paths; callers should only use it for files known to be within the package.
|
|
toSymlink = func(fileName string) string {
|
|
if after, ok := strings.CutPrefix(fileName, realPackageDir); ok {
|
|
return packageDir + after
|
|
}
|
|
return fileName
|
|
}
|
|
return toRealpath, toSymlink
|
|
}
|
|
|
|
type resolutionHost struct {
|
|
fs vfs.FS
|
|
currentDirectory string
|
|
}
|
|
|
|
var _ module.ResolutionHost = (*resolutionHost)(nil)
|
|
|
|
func (rh *resolutionHost) GetCurrentDirectory() string {
|
|
return rh.currentDirectory
|
|
}
|
|
|
|
func (rh *resolutionHost) FS() vfs.FS {
|
|
return rh.fs
|
|
}
|
|
|
|
func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.Resolver {
|
|
rh := &resolutionHost{
|
|
fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpath}),
|
|
currentDirectory: host.GetCurrentDirectory(),
|
|
}
|
|
return module.NewResolverWithOptions(rh, core.EmptyCompilerOptions, "", "", opts)
|
|
}
|