feat(codegen): add TypeScript declarations
Generate .d.ts output for Web Component bundles and add a CLI -dts mode.\n\nCo-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
8bf49c8935
commit
56bd6638db
6 changed files with 124 additions and 14 deletions
|
|
@ -4,10 +4,12 @@
|
|||
// 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
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
goio "io"
|
||||
"os"
|
||||
|
||||
|
|
@ -15,7 +17,17 @@ import (
|
|||
log "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
var emitTypeDefinitions = flag.Bool("dts", false, "emit TypeScript declarations instead of JavaScript")
|
||||
|
||||
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)
|
||||
|
|
@ -26,6 +38,15 @@ func run(r goio.Reader, w goio.Writer) error {
|
|||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -36,7 +57,15 @@ func run(r goio.Reader, w goio.Writer) error {
|
|||
}
|
||||
|
||||
func main() {
|
||||
if err := run(os.Stdin, os.Stdout); err != nil {
|
||||
flag.Parse()
|
||||
|
||||
var err error
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,20 @@ func TestRun_Good(t *testing.T) {
|
|||
assert.Equal(t, 2, strings.Count(js, "extends HTMLElement"))
|
||||
}
|
||||
|
||||
func TestRunTypeDefinitions_Good(t *testing.T) {
|
||||
input := strings.NewReader(`{"H":"nav-bar","C":"main-content"}`)
|
||||
var output bytes.Buffer
|
||||
|
||||
err := runTypeDefinitions(input, &output)
|
||||
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 {};")
|
||||
}
|
||||
|
||||
func TestRun_Bad_InvalidJSON(t *testing.T) {
|
||||
input := strings.NewReader(`not json`)
|
||||
var output bytes.Buffer
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package codegen
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
|
|
@ -69,23 +71,72 @@ func TagToClassName(tag string) string {
|
|||
// GenerateBundle produces all WC class definitions and registrations
|
||||
// for a set of HLCRF slot assignments.
|
||||
func GenerateBundle(slots map[string]string) (string, error) {
|
||||
seen := make(map[string]bool)
|
||||
var b strings.Builder
|
||||
|
||||
for slot, tag := range slots {
|
||||
if seen[tag] {
|
||||
continue
|
||||
}
|
||||
seen[tag] = true
|
||||
|
||||
cls, err := GenerateClass(tag, slot)
|
||||
for _, entry := range sortedSlotEntries(slots) {
|
||||
cls, err := GenerateClass(entry.Tag, entry.Slot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b.WriteString(cls)
|
||||
b.WriteByte('\n')
|
||||
b.WriteString(GenerateRegistration(tag, TagToClassName(tag)))
|
||||
b.WriteString(GenerateRegistration(entry.Tag, TagToClassName(entry.Tag)))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// GenerateTypeDefinitions produces a TypeScript declaration file for the
|
||||
// generated custom elements.
|
||||
func GenerateTypeDefinitions(slots map[string]string) (string, error) {
|
||||
entries := sortedSlotEntries(slots)
|
||||
var b strings.Builder
|
||||
|
||||
for _, entry := range entries {
|
||||
className := TagToClassName(entry.Tag)
|
||||
b.WriteString("declare class ")
|
||||
b.WriteString(className)
|
||||
b.WriteString(" extends HTMLElement {\n")
|
||||
b.WriteString(" connectedCallback(): void;\n")
|
||||
b.WriteString(" render(html: string): void;\n")
|
||||
b.WriteString("}\n")
|
||||
}
|
||||
|
||||
b.WriteString("\ndeclare global {\n")
|
||||
b.WriteString(" interface HTMLElementTagNameMap {\n")
|
||||
for _, entry := range entries {
|
||||
className := TagToClassName(entry.Tag)
|
||||
b.WriteString(` "`)
|
||||
b.WriteString(entry.Tag)
|
||||
b.WriteString(`": `)
|
||||
b.WriteString(className)
|
||||
b.WriteString(";\n")
|
||||
}
|
||||
b.WriteString(" }\n")
|
||||
b.WriteString("}\n")
|
||||
b.WriteString("\nexport {};\n")
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
type slotEntry struct {
|
||||
Slot string
|
||||
Tag string
|
||||
}
|
||||
|
||||
func sortedSlotEntries(slots map[string]string) []slotEntry {
|
||||
keys := slices.Collect(maps.Keys(slots))
|
||||
slices.Sort(keys)
|
||||
|
||||
seen := make(map[string]bool, len(keys))
|
||||
entries := make([]slotEntry, 0, len(keys))
|
||||
for _, slot := range keys {
|
||||
tag := slots[slot]
|
||||
if seen[tag] {
|
||||
continue
|
||||
}
|
||||
seen[tag] = true
|
||||
entries = append(entries, slotEntry{Slot: slot, Tag: tag})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,3 +52,19 @@ func TestGenerateBundle_Good(t *testing.T) {
|
|||
assert.Contains(t, js, "MainContent")
|
||||
assert.Equal(t, 2, strings.Count(js, "extends HTMLElement"))
|
||||
}
|
||||
|
||||
func TestGenerateTypeDefinitions_Good(t *testing.T) {
|
||||
slots := map[string]string{
|
||||
"H": "nav-bar",
|
||||
"C": "main-content",
|
||||
}
|
||||
|
||||
dts, err := GenerateTypeDefinitions(slots)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, dts, "declare global")
|
||||
assert.Contains(t, dts, "class NavBar extends HTMLElement")
|
||||
assert.Contains(t, dts, "class MainContent extends HTMLElement")
|
||||
assert.Contains(t, dts, `"nav-bar": NavBar;`)
|
||||
assert.Contains(t, dts, `"main-content": MainContent;`)
|
||||
assert.Contains(t, dts, "export {};")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ These are not regressions; they are design choices or deferred work recorded for
|
|||
|
||||
4. **Context.service is private.** The i18n service cannot be set after construction or swapped. This is a conservative choice; relaxing it requires deciding whether mutation should be safe for concurrent use.
|
||||
|
||||
5. **TypeScript definitions not generated.** `codegen.GenerateBundle()` produces JS only. A `.d.ts` companion would benefit TypeScript consumers of the generated Web Components.
|
||||
5. **TypeScript definitions are generated.** `codegen.GenerateTypeDefinitions()` produces a `.d.ts` companion for the generated Web Components.
|
||||
|
||||
6. **No CSS scoping helper.** Responsive variants are identified by `data-variant` attributes. Targeting them from CSS requires knowledge of the attribute name. An optional utility for generating scoped CSS selectors is deferred.
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ These are not regressions; they are design choices or deferred work recorded for
|
|||
|
||||
These items were captured during the WASM size reduction work and expert review sessions. They are not committed work items.
|
||||
|
||||
- **TypeScript type definitions** alongside `GenerateBundle()` for typed Web Component consumers.
|
||||
- **TypeScript type definitions** alongside `GenerateBundle()` for typed Web Component consumers. Implemented via `GenerateTypeDefinitions()`.
|
||||
- **Accessibility helpers** — `aria-label`, `alt`, `aria-hidden`, and `tabindex` helpers. The layout has semantic HTML and ARIA roles, and the node layer now exposes common accessibility attribute shortcuts beyond `Attr()`.
|
||||
- **Layout variant validation** — `ValidateLayoutVariant()` and `Layout.VariantError()` report unrecognised slot characters while preserving silent render-time skipping.
|
||||
- **Daemon mode for codegen** — watch mode for regenerating the JS bundle when slot config changes, for development workflows.
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ This builds a Header-Content-Footer layout with semantic HTML elements (`<header
|
|||
| `render.go` | `Render()` convenience function |
|
||||
| `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 (closed Shadow DOM) |
|
||||
| `cmd/codegen/main.go` | Build-time CLI: JSON slot map on stdin, JS bundle on stdout |
|
||||
| `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/wasm/main.go` | WASM entry point exporting `renderToString()` to JavaScript |
|
||||
|
||||
## Key Concepts
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue