feat(06-02): create Go→TypeScript event constant codegen tool
- Add backend/events/cmd/genevents/main.go using go/ast for deterministic output - Add //go:generate directive to backend/events/events.go - Generate frontend/src/events.ts with all 21 constants including LibraryConfigChanged - Atomic file writes via temp file + rename - Comment groups preserved with trailing period stripping
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
// the corresponding event names in the TypeScript frontend.
|
||||
package events
|
||||
|
||||
//go:generate go run ./cmd/genevents -source events.go -output ../../frontend/src/events.ts
|
||||
|
||||
// Playback events (backend → frontend push).
|
||||
const (
|
||||
PlaybackStateChanged = "PlaybackStateChanged"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Centralized event name constants for Wails frontend/backend communication.
|
||||
// These names must match the corresponding event names in the Go backend.
|
||||
// Code generated by genevents from backend/events/events.go. DO NOT EDIT.
|
||||
|
||||
export const Events = {
|
||||
// Playback events (backend → frontend push)
|
||||
@@ -15,6 +14,12 @@ export const Events = {
|
||||
QueueModeChanged: "QueueModeChanged",
|
||||
QueueTracksModified: "QueueTracksModified",
|
||||
|
||||
// Config events
|
||||
LibraryConfigChanged: "LibraryConfigChanged",
|
||||
ThemeConfigChanged: "ThemeConfigChanged",
|
||||
TrackListConfigChanged: "TrackListConfigChanged",
|
||||
FavoritesConfigChanged: "FavoritesConfigChanged",
|
||||
|
||||
// Playlist events
|
||||
PlaylistCreated: "PlaylistCreated",
|
||||
PlaylistDeleted: "PlaylistDeleted",
|
||||
@@ -23,11 +28,6 @@ export const Events = {
|
||||
PlaylistsRestored: "PlaylistsRestored",
|
||||
DefaultPlaylistChanged: "DefaultPlaylistChanged",
|
||||
|
||||
// Config events
|
||||
ThemeConfigChanged: "ThemeConfigChanged",
|
||||
TrackListConfigChanged: "TrackListConfigChanged",
|
||||
FavoritesConfigChanged: "FavoritesConfigChanged",
|
||||
|
||||
// Library events
|
||||
LibraryScanStarted: "LibraryScanStarted",
|
||||
LibraryScanComplete: "LibraryScanComplete",
|
||||
|
||||
Reference in New Issue
Block a user