From a928d01b9e8ef02cc33ee1792775a6b0ff90110f Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 19:52:29 +0000 Subject: [PATCH] feat(codegen): add TypeScript CLI output Co-Authored-By: Virgil --- cmd/codegen/main.go | 28 +++++++++++++++++++--------- cmd/codegen/main_test.go | 23 +++++++++++++++++++---- docs/development.md | 8 ++++++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/cmd/codegen/main.go b/cmd/codegen/main.go index 7f26357..968d38b 100644 --- a/cmd/codegen/main.go +++ b/cmd/codegen/main.go @@ -1,14 +1,16 @@ //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. +// 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 package main import ( + "flag" goio "io" "os" @@ -18,7 +20,7 @@ import ( log "dappco.re/go/core/log" ) -func run(r goio.Reader, w goio.Writer) error { +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) @@ -30,19 +32,27 @@ func run(r goio.Reader, w goio.Writer) error { return log.E("codegen", "invalid JSON", err) } - js, err := codegen.GenerateBundle(slots) - if err != nil { - return log.E("codegen", "generate bundle", err) + out := "" + if emitTypes { + out = codegen.GenerateTypeScriptDefinitions(slots) + } else { + out, err = codegen.GenerateBundle(slots) + if err != nil { + return log.E("codegen", "generate bundle", err) + } } - _, err = goio.WriteString(w, js) + _, err = goio.WriteString(w, out) if err != nil { - return log.E("codegen", "writing bundle", err) + return log.E("codegen", "writing output", err) } return nil } func main() { + emitTypes := flag.Bool("types", false, "emit TypeScript declarations instead of JavaScript") + flag.Parse() + 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)) @@ -60,7 +70,7 @@ func main() { _ = stdout.Close() }() - if err := run(stdin, stdout); err != nil { + if err := run(stdin, stdout, *emitTypes); err != nil { log.Error("codegen failed", "scope", "codegen.main", "err", err) os.Exit(1) } diff --git a/cmd/codegen/main_test.go b/cmd/codegen/main_test.go index d0046e3..4cda81b 100644 --- a/cmd/codegen/main_test.go +++ b/cmd/codegen/main_test.go @@ -14,7 +14,7 @@ func TestRun_WritesBundle_Good(t *testing.T) { input := core.NewReader(`{"H":"nav-bar","C":"main-content"}`) output := core.NewBuilder() - err := run(input, output) + err := run(input, output, false) require.NoError(t, err) js := output.String() @@ -28,7 +28,7 @@ func TestRun_InvalidJSON_Bad(t *testing.T) { input := core.NewReader(`not json`) output := core.NewBuilder() - err := run(input, output) + err := run(input, output, false) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid JSON") } @@ -37,7 +37,7 @@ func TestRun_InvalidTag_Bad(t *testing.T) { input := core.NewReader(`{"H":"notag"}`) output := core.NewBuilder() - err := run(input, output) + err := run(input, output, false) assert.Error(t, err) assert.Contains(t, err.Error(), "hyphen") } @@ -46,11 +46,26 @@ func TestRun_EmptySlots_Good(t *testing.T) { input := core.NewReader(`{}`) output := core.NewBuilder() - err := run(input, output) + err := run(input, output, false) require.NoError(t, err) assert.Empty(t, output.String()) } +func TestRun_WritesTypeScriptDefinitions_Good(t *testing.T) { + input := core.NewReader(`{"H":"nav-bar","C":"main-content"}`) + output := core.NewBuilder() + + err := run(input, output, true) + require.NoError(t, err) + + dts := output.String() + assert.Contains(t, dts, "declare global") + assert.Contains(t, dts, `"nav-bar": NavBar;`) + assert.Contains(t, dts, `"main-content": MainContent;`) + assert.Contains(t, dts, "export declare class NavBar extends HTMLElement") + assert.Contains(t, dts, "export declare class MainContent extends HTMLElement") +} + func countSubstr(s, substr string) int { if substr == "" { return len(s) + 1 diff --git a/docs/development.md b/docs/development.md index 7ee9fec..aac0d5f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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. +Pass `-types` to emit ambient TypeScript declarations instead of JavaScript: + +```bash +echo '{"H":"site-header","C":"app-content"}' \ + | go run ./cmd/codegen/ -types \ + > components.d.ts +``` + To test the CLI: ```bash