feat(codegen): restore watch mode
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
afa0337bbd
commit
3d2fdf4e22
5 changed files with 145 additions and 22 deletions
|
|
@ -1,23 +1,37 @@
|
|||
//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.
|
||||
// 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)
|
||||
|
|
@ -33,37 +47,83 @@ func runWithMode(r goio.Reader, w goio.Writer, emitTypes bool) error {
|
|||
return log.E("codegen", "reading stdin", err)
|
||||
}
|
||||
|
||||
var slots map[string]string
|
||||
if err := json.Unmarshal(data, &slots); err != nil {
|
||||
return log.E("codegen", "invalid JSON", err)
|
||||
}
|
||||
|
||||
if emitTypes {
|
||||
dts, err := codegen.GenerateTypeDefinitions(slots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = goio.WriteString(w, dts)
|
||||
return err
|
||||
}
|
||||
|
||||
js, err := codegen.GenerateBundle(slots)
|
||||
out, err := generate(data, emitTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = goio.WriteString(w, js)
|
||||
_, 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
|
||||
for {
|
||||
input, err := coreio.Local.Read(inputPath)
|
||||
if err != nil {
|
||||
return log.E("codegen", "reading input file", err)
|
||||
}
|
||||
|
||||
if input != lastInput {
|
||||
out, err := generate([]byte(input), emitTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := coreio.Local.Write(outputPath, out); err != nil {
|
||||
return log.E("codegen", "writing output file", err)
|
||||
}
|
||||
lastInput = input
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return ctx.Err()
|
||||
case <-time.After(pollInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var err error
|
||||
if *emitTypeDefinitions {
|
||||
err = runTypeDefinitions(os.Stdin, os.Stdout)
|
||||
if *watchMode {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
err = runDaemon(ctx, *watchInputPath, *watchOutputPath, *emitTypeDefinitions, *watchPollInterval)
|
||||
} else {
|
||||
err = run(os.Stdin, os.Stdout)
|
||||
if *emitTypeDefinitions {
|
||||
err = runTypeDefinitions(os.Stdin, os.Stdout)
|
||||
} else {
|
||||
err = run(os.Stdin, os.Stdout)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("codegen failed", "err", err)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
//go:build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -63,3 +68,48 @@ func TestRun_Good_Empty(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Empty(t, output.String())
|
||||
}
|
||||
|
||||
func TestRunDaemon_WritesBundle(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
inputPath := dir + "/slots.json"
|
||||
outputPath := dir + "/bundle.js"
|
||||
|
||||
require.NoError(t, writeTestFile(inputPath, `{"H":"nav-bar","C":"main-content"}`))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- runDaemon(ctx, inputPath, outputPath, false, 5*time.Millisecond)
|
||||
}()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
got, err := readTestFile(outputPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(got, "NavBar") && strings.Contains(got, "MainContent")
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
cancel()
|
||||
require.NoError(t, <-done)
|
||||
}
|
||||
|
||||
func TestRunDaemon_MissingPaths(t *testing.T) {
|
||||
err := runDaemon(context.Background(), "", "", false, time.Millisecond)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "watch mode requires -input")
|
||||
}
|
||||
|
||||
func writeTestFile(path, content string) error {
|
||||
return os.WriteFile(path, []byte(content), 0o600)
|
||||
}
|
||||
|
||||
func readTestFile(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
|
|
|||
5
deps/go-io/io.go
vendored
5
deps/go-io/io.go
vendored
|
|
@ -15,3 +15,8 @@ func (localFS) Read(path string) (string, error) {
|
|||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Write stores content at path, replacing any existing file.
|
||||
func (localFS) Write(path, content string) error {
|
||||
return os.WriteFile(path, []byte(content), 0o600)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,14 @@ echo '{"H":"site-header","C":"app-content","F":"site-footer"}' \
|
|||
|
||||
JSON keys are HLCRF slot letters (`H`, `L`, `C`, `R`, `F`). Values are custom element tag names (must contain a hyphen per the Web Components specification). Duplicate tag values are deduplicated.
|
||||
|
||||
To run the daemon mode, point it at an input JSON file and an output bundle path:
|
||||
|
||||
```bash
|
||||
go run ./cmd/codegen/ -watch -input slots.json -output components.js
|
||||
```
|
||||
|
||||
Add `-dts` to emit TypeScript declarations instead of JavaScript in either mode.
|
||||
|
||||
To test the CLI:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ This builds a Header-Content-Footer layout with semantic HTML elements (`<header
|
|||
| `path.go` | `ParseBlockID()` for decoding `data-block` path attributes |
|
||||
| `pipeline.go` | `StripTags`, `Imprint`, `CompareVariants` (server-side only, `!js` build tag) |
|
||||
| `codegen/codegen.go` | Web Component class generation and TypeScript declarations (closed Shadow DOM) |
|
||||
| `cmd/codegen/main.go` | Build-time CLI: JSON slot map on stdin, JS bundle on stdout, `-dts` for `.d.ts` output |
|
||||
| `cmd/codegen/main.go` | Build-time CLI: JSON slot map on stdin, JS bundle on stdout, `-dts` for `.d.ts` output, `-watch` for file polling |
|
||||
| `cmd/wasm/main.go` | WASM entry point exporting `renderToString()` to JavaScript |
|
||||
|
||||
## Key Concepts
|
||||
|
|
@ -60,7 +60,7 @@ This builds a Header-Content-Footer layout with semantic HTML elements (`<header
|
|||
|
||||
**Grammar pipeline** -- Server-side only. `Imprint()` renders a node tree to HTML, strips tags, tokenises the plain text via `go-i18n/reversal`, and returns a `GrammarImprint` for semantic analysis. `CompareVariants()` computes pairwise similarity scores across responsive variants.
|
||||
|
||||
**Web Component codegen** -- `cmd/codegen/` generates ES2022 Web Component classes with closed Shadow DOM from a JSON slot-to-tag mapping. This is a build-time tool, not used at runtime.
|
||||
**Web Component codegen** -- `cmd/codegen/` generates ES2022 Web Component classes with closed Shadow DOM from a JSON slot-to-tag mapping. This is a build-time tool, not used at runtime. It also supports `-watch` for polling an input JSON file and rewriting an output bundle in place.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue