go-html/cmd/codegen/main.go
Virgil 14c16b5385
All checks were successful
Security Scan / security (push) Successful in 9s
Test / test (push) Successful in 58s
feat(codegen): add watch mode for bundle generation
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 20:04:21 +00:00

181 lines
4.3 KiB
Go

//go:build !js
// Package main provides a build-time CLI for generating Web Component bundles.
// Reads a JSON slot map from stdin, writes the generated JS or TypeScript to stdout.
//
// Usage:
//
// echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ > components.js
// echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ -types > components.d.ts
// echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ -watch -input slots.json -output components.js
package main
import (
"context"
"errors"
"flag"
goio "io"
"os"
"os/signal"
"time"
core "dappco.re/go/core"
"dappco.re/go/core/html/codegen"
coreio "dappco.re/go/core/io"
log "dappco.re/go/core/log"
)
func generate(data []byte, emitTypes bool) (string, error) {
var slots map[string]string
if result := core.JSONUnmarshal(data, &slots); !result.OK {
err, _ := result.Value.(error)
return "", log.E("codegen", "invalid JSON", err)
}
if emitTypes {
return codegen.GenerateTypeScriptDefinitions(slots), nil
}
out, err := codegen.GenerateBundle(slots)
if err != nil {
return "", log.E("codegen", "generate bundle", err)
}
return out, nil
}
func run(r goio.Reader, w goio.Writer, emitTypes bool) error {
data, err := goio.ReadAll(r)
if err != nil {
return log.E("codegen", "reading stdin", err)
}
out, err := generate(data, emitTypes)
if err != nil {
return err
}
_, err = goio.WriteString(w, out)
if err != nil {
return log.E("codegen", "writing output", err)
}
return nil
}
func runDaemon(ctx context.Context, inputPath, outputPath string, emitTypes bool, pollInterval time.Duration) error {
if inputPath == "" {
return log.E("codegen", "watch mode requires -input", nil)
}
if outputPath == "" {
return log.E("codegen", "watch mode requires -output", nil)
}
if pollInterval <= 0 {
pollInterval = 250 * time.Millisecond
}
var lastInput []byte
for {
input, err := readLocalFile(inputPath)
if err != nil {
return log.E("codegen", "reading input file", err)
}
if !sameBytes(input, lastInput) {
out, err := generate(input, emitTypes)
if err != nil {
return err
}
if err := writeLocalFile(outputPath, out); err != nil {
return log.E("codegen", "writing output file", err)
}
lastInput = append(lastInput[:0], input...)
}
select {
case <-ctx.Done():
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
case <-time.After(pollInterval):
}
}
}
func readLocalFile(path string) ([]byte, error) {
f, err := coreio.Local.Open(path)
if err != nil {
return nil, err
}
defer func() {
_ = f.Close()
}()
return goio.ReadAll(f)
}
func writeLocalFile(path, content string) error {
f, err := coreio.Local.Create(path)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()
_, err = goio.WriteString(f, content)
return err
}
func sameBytes(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range len(a) {
if a[i] != b[i] {
return false
}
}
return true
}
func main() {
emitWatch := flag.Bool("watch", false, "poll input and rewrite output when the JSON changes")
inputPath := flag.String("input", "", "path to the JSON slot map used by -watch")
outputPath := flag.String("output", "", "path to the generated bundle written by -watch")
emitTypes := flag.Bool("types", false, "emit TypeScript declarations instead of JavaScript")
pollInterval := flag.Duration("poll", 250*time.Millisecond, "poll interval used by -watch")
flag.Parse()
if *emitWatch {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := runDaemon(ctx, *inputPath, *outputPath, *emitTypes, *pollInterval); err != nil {
log.Error("codegen failed", "scope", "codegen.main", "err", err)
os.Exit(1)
}
return
}
stdin, err := coreio.Local.Open("/dev/stdin")
if err != nil {
log.Error("failed to open stdin", "scope", "codegen.main", "err", log.E("codegen.main", "open stdin", err))
os.Exit(1)
}
stdout, err := coreio.Local.Create("/dev/stdout")
if err != nil {
_ = stdin.Close()
log.Error("failed to open stdout", "scope", "codegen.main", "err", log.E("codegen.main", "open stdout", err))
os.Exit(1)
}
defer func() {
_ = stdin.Close()
_ = stdout.Close()
}()
if err := run(stdin, stdout, *emitTypes); err != nil {
log.Error("codegen failed", "scope", "codegen.main", "err", err)
os.Exit(1)
}
}