- 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
179 lines
3.9 KiB
Go
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)
|
|
}
|