Files
yellowjacket/backend/events/cmd/genevents/main.go
T
yonlu e1a95e65a9 fix(quick-13): resolve lint issues in main source files
- Fix errcheck for db.Close() in testhelper.go
- Fix errcheck, nlreturn, wsl, gofumpt issues in genevents/main.go
- Fix gofumpt and wsl issues in library.go
2026-03-05 14:07:50 -05:00

179 lines
3.9 KiB
Go

// Command genevents reads Go event constants from events.go using go/ast
// and generates the corresponding TypeScript constants file.
//
// Usage:
//
// go run ./cmd/genevents -source events.go -output ../../frontend/src/events.ts
package main
import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
)
func main() {
source := flag.String("source", "events.go", "path to Go events source file")
output := flag.String("output", "", "path to TypeScript output file (stdout if empty)")
flag.Parse()
consts, err := parseEvents(*source)
if err != nil {
fmt.Fprintf(os.Stderr, "genevents: %v\n", err)
os.Exit(1)
}
ts := generateTypeScript(consts)
if *output == "" || *output == "/dev/stdout" {
fmt.Print(ts)
return
}
if err := writeAtomic(*output, ts); err != nil {
fmt.Fprintf(os.Stderr, "genevents: write %s: %v\n", *output, err)
os.Exit(1)
}
}
// constGroup holds a block of related constants with its doc comment.
type constGroup struct {
Comment string // doc comment text (empty if none)
Consts []constEntry
}
// constEntry holds one constant name and its string value.
type constEntry struct {
Name string
Value string
}
// parseEvents parses the Go source file and extracts typed string constant
// groups in declaration order.
func parseEvents(path string) ([]constGroup, error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
var groups []constGroup
for _, decl := range f.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.CONST {
continue
}
var g constGroup
// Extract doc comment from the const block.
if gd.Doc != nil {
g.Comment = cleanComment(gd.Doc.Text())
}
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, name := range vs.Names {
if i >= len(vs.Values) {
continue
}
bl, ok := vs.Values[i].(*ast.BasicLit)
if !ok || bl.Kind != token.STRING {
continue
}
// Strip quotes from the string literal value.
val := strings.Trim(bl.Value, `"`)
g.Consts = append(g.Consts, constEntry{Name: name.Name, Value: val})
}
}
if len(g.Consts) > 0 {
groups = append(groups, g)
}
}
return groups, nil
}
// cleanComment trims whitespace and strips trailing periods from Go doc
// comment text (Go convention uses periods; TypeScript comments typically
// do not).
func cleanComment(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimSuffix(s, ".")
return s
}
// generateTypeScript produces the full TypeScript source from the parsed
// constant groups.
func generateTypeScript(groups []constGroup) string {
var b strings.Builder
b.WriteString("// Code generated by genevents from backend/events/events.go. DO NOT EDIT.\n")
b.WriteString("\n")
b.WriteString("export const Events = {\n")
for i, g := range groups {
if g.Comment != "" {
b.WriteString(" // " + g.Comment + "\n")
}
for _, c := range g.Consts {
b.WriteString(fmt.Sprintf(" %s: %q,\n", c.Name, c.Value))
}
// Blank line between groups, but not after the last one.
if i < len(groups)-1 {
b.WriteString("\n")
}
}
b.WriteString("} as const;\n")
b.WriteString("\n")
b.WriteString("export type EventName = (typeof Events)[keyof typeof Events];\n")
return b.String()
}
// writeAtomic writes data to a temporary file in the same directory as path,
// then renames it into place for atomic replacement.
func writeAtomic(path, data string) error {
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, ".genevents-*.tmp")
if err != nil {
return err
}
tmpName := tmp.Name()
if _, err := tmp.WriteString(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpName)
return err
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpName)
return err
}
return os.Rename(tmpName, path)
}