diff --git a/backend/events/cmd/genevents/main.go b/backend/events/cmd/genevents/main.go new file mode 100644 index 0000000..61804da --- /dev/null +++ b/backend/events/cmd/genevents/main.go @@ -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) +} diff --git a/backend/events/events.go b/backend/events/events.go index 9b2c263..c298213 100644 --- a/backend/events/events.go +++ b/backend/events/events.go @@ -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" diff --git a/frontend/src/events.ts b/frontend/src/events.ts index e46d223..e8f8d9c 100644 --- a/frontend/src/events.ts +++ b/frontend/src/events.ts @@ -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",