1
0
Fork 0
golang-github-pocketbase-ty.../tygoja.go
Daniel Baumann b6e042e2af
Adding upstream version 0.0~git20250307.c2e6a77.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-22 11:21:48 +02:00

264 lines
5.7 KiB
Go

package tygoja
import (
"fmt"
"path/filepath"
"regexp"
"strings"
"golang.org/x/tools/go/packages"
)
// Tygoja is a generator for one or more input packages,
// responsible for linking them together if necessary.
type Tygoja struct {
conf *Config
parent *Tygoja
implicitPackages map[string][]string
generatedTypes map[string][]string
generatedPackageDocs map[string]struct{}
}
// New initializes a new Tygoja generator from the specified config.
func New(config Config) *Tygoja {
config.InitDefaults()
return &Tygoja{
conf: &config,
implicitPackages: map[string][]string{},
generatedTypes: map[string][]string{},
generatedPackageDocs: map[string]struct{}{},
}
}
// Generate executes the generator and produces the related TS files.
func (g *Tygoja) Generate() (string, error) {
// extract config packages
configPackages := make([]string, 0, len(g.conf.Packages))
for p, types := range g.conf.Packages {
if len(types) == 0 {
continue // no typings
}
configPackages = append(configPackages, p)
}
// load packages info
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedSyntax | packages.NeedFiles | packages.NeedDeps | packages.NeedImports | packages.NeedTypes,
}, configPackages...)
if err != nil {
return "", err
}
var s strings.Builder
// Heading
if g.parent == nil {
s.WriteString("// GENERATED CODE - DO NOT MODIFY BY HAND\n")
if g.conf.Heading != "" {
s.WriteString(g.conf.Heading)
}
// write base types
// ---
s.WriteString("type ")
s.WriteString(BaseTypeDict)
s.WriteString(" = { [key:string | number | symbol]: any; }\n")
s.WriteString("type ")
s.WriteString(BaseTypeAny)
s.WriteString(" = any\n")
// ---
}
for i, pkg := range pkgs {
if len(pkg.Errors) > 0 {
return "", fmt.Errorf("%+v", pkg.Errors)
}
if len(pkg.GoFiles) == 0 {
return "", fmt.Errorf("no input go files for package index %d", i)
}
if len(g.conf.Packages[pkg.ID]) == 0 {
// ignore the package as it has no typings
continue
}
pkgGen := &PackageGenerator{
conf: g.conf,
pkg: pkg,
types: g.conf.Packages[pkg.ID],
withPkgDoc: !g.isPackageDocGenerated(pkg.ID),
generatedTypes: map[string]struct{}{},
unknownTypes: map[string]struct{}{},
imports: map[string][]string{},
}
code, err := pkgGen.Generate()
if err != nil {
return "", err
}
g.generatedPackageDocs[pkg.ID] = struct{}{}
for t := range pkgGen.generatedTypes {
g.generatedTypes[pkg.ID] = append(g.generatedTypes[pkg.ID], t)
}
for t := range pkgGen.unknownTypes {
parts := strings.Split(t, ".")
var tPkg string
var tName string
if len(parts) == 0 {
continue
}
if len(parts) == 2 {
// type from external package
tPkg = parts[0]
tName = parts[1]
} else {
// unexported type from the current package
tName = parts[0]
// already mapped for export
if pkgGen.isTypeAllowed(tName) {
continue
}
tPkg = packageNameFromPath(pkg.ID)
// add to self import later
pkgGen.imports[pkg.ID] = []string{tPkg}
}
for p, aliases := range pkgGen.imports {
for _, alias := range aliases {
if tName != "" && alias == tPkg && !g.isTypeGenerated(p, tName) && !exists(g.implicitPackages[p], tName) {
if g.implicitPackages[p] == nil {
g.implicitPackages[p] = []string{}
}
g.implicitPackages[p] = append(g.implicitPackages[p], tName)
break
}
}
}
}
s.WriteString(code)
}
// recursively try to generate the found unknown types
if len(g.implicitPackages) > 0 {
subConfig := *g.conf
subConfig.Heading = ""
if (subConfig.TypeMappings) == nil {
subConfig.TypeMappings = map[string]string{}
}
// extract the nonempty package definitions
subConfig.Packages = make(map[string][]string, len(g.implicitPackages))
for p, types := range g.implicitPackages {
if len(types) == 0 {
continue
}
subConfig.Packages[p] = types
}
subGenerator := New(subConfig)
subGenerator.parent = g
subResult, err := subGenerator.Generate()
if err != nil {
return "", err
}
s.WriteString(subResult)
}
return s.String(), nil
}
func (g *Tygoja) isPackageDocGenerated(pkgId string) bool {
_, ok := g.generatedPackageDocs[pkgId]
if ok {
return true
}
if g.parent != nil {
return g.parent.isPackageDocGenerated(pkgId)
}
return false
}
func (g *Tygoja) isTypeGenerated(pkg string, name string) bool {
if g.parent != nil && g.parent.isTypeGenerated(pkg, name) {
return true
}
if len(g.generatedTypes[pkg]) == 0 {
return false
}
for _, t := range g.generatedTypes[pkg] {
if t == name {
return true
}
}
return false
}
// isTypeAllowed checks whether the provided type name is allowed by the generator "types".
func (g *PackageGenerator) isTypeAllowed(name string) bool {
name = strings.TrimSpace(name)
if name == "" {
return false
}
for _, t := range g.types {
if t == name || t == "*" {
return true
}
}
return false
}
func (g *PackageGenerator) markTypeAsGenerated(t string) {
g.generatedTypes[t] = struct{}{}
}
var versionRegex = regexp.MustCompile(`^v\d+$`)
// packageNameFromPath extracts and normalizes the imported package identifier.
//
// For example:
//
// "github.com/labstack/echo/v5" -> "echo"
// "github.com/go-ozzo/ozzo-validation/v4" -> "ozzo_validation"
func packageNameFromPath(path string) string {
name := filepath.Base(strings.Trim(path, `"' `))
if versionRegex.MatchString(name) {
name = filepath.Base(filepath.Dir(path))
}
return strings.ReplaceAll(name, "-", "_")
}
// exists checks if search exists in list.
func exists[T comparable](list []T, search T) bool {
for _, v := range list {
if v == search {
return true
}
}
return false
}