go-html/codegen/codegen.go
Virgil 56bd6638db
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
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>
2026-04-03 16:42:41 +00:00

142 lines
3.8 KiB
Go

package codegen
import (
"fmt"
"maps"
"slices"
"strings"
"text/template"
log "dappco.re/go/core/log"
)
// wcTemplate is the Web Component class template.
// Uses closed Shadow DOM for isolation. Content is set via the shadow root's
// DOM API using trusted go-html codegen output (never user input).
var wcTemplate = template.Must(template.New("wc").Parse(`class {{.ClassName}} extends HTMLElement {
#shadow;
constructor() {
super();
this.#shadow = this.attachShadow({ mode: "closed" });
}
connectedCallback() {
this.#shadow.textContent = "";
const slot = this.getAttribute("data-slot") || "{{.Slot}}";
this.dispatchEvent(new CustomEvent("wc-ready", { detail: { tag: "{{.Tag}}", slot } }));
}
render(html) {
const tpl = document.createElement("template");
tpl.insertAdjacentHTML("afterbegin", html);
this.#shadow.textContent = "";
this.#shadow.appendChild(tpl.content.cloneNode(true));
}
}`))
// GenerateClass produces a JS class definition for a custom element.
func GenerateClass(tag, slot string) (string, error) {
if !strings.Contains(tag, "-") {
return "", log.E("codegen.GenerateClass", "custom element tag must contain a hyphen: "+tag, nil)
}
var b strings.Builder
err := wcTemplate.Execute(&b, struct {
ClassName, Tag, Slot string
}{
ClassName: TagToClassName(tag),
Tag: tag,
Slot: slot,
})
if err != nil {
return "", log.E("codegen.GenerateClass", "template exec", err)
}
return b.String(), nil
}
// GenerateRegistration produces the customElements.define() call.
func GenerateRegistration(tag, className string) string {
return fmt.Sprintf(`customElements.define("%s", %s);`, tag, className)
}
// TagToClassName converts a kebab-case tag to PascalCase class name.
func TagToClassName(tag string) string {
var b strings.Builder
for p := range strings.SplitSeq(tag, "-") {
if len(p) > 0 {
b.WriteString(strings.ToUpper(p[:1]))
b.WriteString(p[1:])
}
}
return b.String()
}
// GenerateBundle produces all WC class definitions and registrations
// for a set of HLCRF slot assignments.
func GenerateBundle(slots map[string]string) (string, error) {
var b strings.Builder
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(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
}