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.
|
// the corresponding event names in the TypeScript frontend.
|
||||||
package events
|
package events
|
||||||
|
|
||||||
|
//go:generate go run ./cmd/genevents -source events.go -output ../../frontend/src/events.ts
|
||||||
|
|
||||||
// Playback events (backend → frontend push).
|
// Playback events (backend → frontend push).
|
||||||
const (
|
const (
|
||||||
PlaybackStateChanged = "PlaybackStateChanged"
|
PlaybackStateChanged = "PlaybackStateChanged"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Centralized event name constants for Wails frontend/backend communication.
|
// Code generated by genevents from backend/events/events.go. DO NOT EDIT.
|
||||||
// These names must match the corresponding event names in the Go backend.
|
|
||||||
|
|
||||||
export const Events = {
|
export const Events = {
|
||||||
// Playback events (backend → frontend push)
|
// Playback events (backend → frontend push)
|
||||||
@@ -15,6 +14,12 @@ export const Events = {
|
|||||||
QueueModeChanged: "QueueModeChanged",
|
QueueModeChanged: "QueueModeChanged",
|
||||||
QueueTracksModified: "QueueTracksModified",
|
QueueTracksModified: "QueueTracksModified",
|
||||||
|
|
||||||
|
// Config events
|
||||||
|
LibraryConfigChanged: "LibraryConfigChanged",
|
||||||
|
ThemeConfigChanged: "ThemeConfigChanged",
|
||||||
|
TrackListConfigChanged: "TrackListConfigChanged",
|
||||||
|
FavoritesConfigChanged: "FavoritesConfigChanged",
|
||||||
|
|
||||||
// Playlist events
|
// Playlist events
|
||||||
PlaylistCreated: "PlaylistCreated",
|
PlaylistCreated: "PlaylistCreated",
|
||||||
PlaylistDeleted: "PlaylistDeleted",
|
PlaylistDeleted: "PlaylistDeleted",
|
||||||
@@ -23,11 +28,6 @@ export const Events = {
|
|||||||
PlaylistsRestored: "PlaylistsRestored",
|
PlaylistsRestored: "PlaylistsRestored",
|
||||||
DefaultPlaylistChanged: "DefaultPlaylistChanged",
|
DefaultPlaylistChanged: "DefaultPlaylistChanged",
|
||||||
|
|
||||||
// Config events
|
|
||||||
ThemeConfigChanged: "ThemeConfigChanged",
|
|
||||||
TrackListConfigChanged: "TrackListConfigChanged",
|
|
||||||
FavoritesConfigChanged: "FavoritesConfigChanged",
|
|
||||||
|
|
||||||
// Library events
|
// Library events
|
||||||
LibraryScanStarted: "LibraryScanStarted",
|
LibraryScanStarted: "LibraryScanStarted",
|
||||||
LibraryScanComplete: "LibraryScanComplete",
|
LibraryScanComplete: "LibraryScanComplete",
|
||||||
|
|||||||
Reference in New Issue
Block a user