go-html/cmd/codegen/main.go
Virgil 575150d686
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
fix(codegen): keep watch mode alive on missing input files
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 01:19:44 +00:00

153 lines
3.9 KiB
Go

//go:build !js
// Package main provides a build-time CLI for generating Web Component JS bundles.
// Reads a JSON slot map from stdin, writes the generated JS to stdout, and can
// optionally watch a slot file and rewrite an output bundle on change.
//
// 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/ -dts > components.d.ts
// echo '{"H":"nav-bar","C":"main-content"}' > slots.json
// go run ./cmd/codegen/ -watch -input slots.json -output components.js
package main
import (
"context"
"encoding/json"
"errors"
"flag"
goio "io"
"os"
"os/signal"
"time"
"dappco.re/go/core/html/codegen"
coreio "dappco.re/go/core/io"
log "dappco.re/go/core/log"
)
var emitTypeDefinitions = flag.Bool("dts", false, "emit TypeScript declarations instead of JavaScript")
var watchMode = flag.Bool("watch", false, "poll an input file and rewrite an output bundle when it changes")
var watchInputPath = flag.String("input", "", "path to the JSON slot map used by -watch")
var watchOutputPath = flag.String("output", "", "path to the generated bundle written by -watch")
var watchPollInterval = flag.Duration("poll", 250*time.Millisecond, "poll interval used by -watch")
func run(r goio.Reader, w goio.Writer) error {
return runWithMode(r, w, false)
}
func runTypeDefinitions(r goio.Reader, w goio.Writer) error {
return runWithMode(r, w, true)
}
func runWithMode(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)
return err
}
func generate(data []byte, emitTypes bool) (string, error) {
var slots map[string]string
if err := json.Unmarshal(data, &slots); err != nil {
return "", log.E("codegen", "invalid JSON", err)
}
if emitTypes {
return codegen.GenerateTypeDefinitions(slots)
}
return codegen.GenerateBundle(slots)
}
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 string
var lastOutput string
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for {
input, err := coreio.Local.Read(inputPath)
if err != nil {
if os.IsNotExist(err) {
select {
case <-ctx.Done():
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
case <-ticker.C:
}
continue
}
return log.E("codegen", "reading input file", err)
}
if input != lastInput {
out, err := generate([]byte(input), emitTypes)
if err != nil {
// Watch mode should keep running through transient bad edits.
log.Error("codegen watch skipped invalid input", "err", err)
lastInput = input
} else {
if out != lastOutput {
if err := coreio.Local.Write(outputPath, out); err != nil {
return log.E("codegen", "writing output file", err)
}
lastOutput = out
}
lastInput = input
}
}
select {
case <-ctx.Done():
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
case <-ticker.C:
}
}
}
func main() {
flag.Parse()
var err error
if *watchMode {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
err = runDaemon(ctx, *watchInputPath, *watchOutputPath, *emitTypeDefinitions, *watchPollInterval)
} else {
if *emitTypeDefinitions {
err = runTypeDefinitions(os.Stdin, os.Stdout)
} else {
err = run(os.Stdin, os.Stdout)
}
}
if err != nil {
log.Error("codegen failed", "err", err)
os.Exit(1)
}
}