// 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) }