Files
mesh-catalog/modules/mesh-build-go/buildstep/seatgen/tsgen.go
T

233 lines
7.2 KiB
Go

package seatgen
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
const tsHeader = "// Code generated by seatgen from the seat definitions; DO NOT EDIT.\n\n"
// TSSeatDir is where a seat defined whole has its npm package (ADR 0625 §1): the indexer counts the npm package a
// lock resolves, so each such seat is a package of its own and a module is rebuilt only for the seats it uses.
func TSSeatDir(seat string) string { return "ts/seats/" + pkgName(seat) }
// TSSeatPackage is the npm package name of a seat defined whole, beside the seat set's package root.
func TSSeatPackage(root, seat string) string { return root + "-" + seat }
// tsConfig type-checks the package's sources and writes nothing: the package ships its TypeScript, which its importers
// compile or bundle, so it has no build script and needs no compiler of its own (F7: a generated package has no
// dependency, and its lock, which the build module requires of a package artifact, holds none).
const tsConfig = `{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"]
}
`
// tsPackage writes one npm package's package.json, its dependency-free package-lock.json and its tsconfig.json at dir.
func tsPackage(out map[string][]byte, dir, name, description string) {
pkg := map[string]any{
"name": name, "version": "0.1.0", "type": "module",
"description": description,
"exports": map[string]any{".": "./src/index.ts"},
"types": "./src/index.ts",
"files": []any{"src"},
}
raw, _ := json.MarshalIndent(pkg, "", " ")
out[dir+"/package.json"] = append(raw, '\n')
lock := map[string]any{
"name": name, "version": "0.1.0", "lockfileVersion": 3, "requires": true,
"packages": map[string]any{"": map[string]any{"name": name, "version": "0.1.0"}},
}
raw, _ = json.MarshalIndent(lock, "", " ")
out[dir+"/package-lock.json"] = append(raw, '\n')
out[dir+"/tsconfig.json"] = []byte(tsConfig)
}
// tsFiles writes the npm side: one package per seat defined whole at TSSeatDir (ADR 0625 §1), holding the parts it includes whole and importing nothing outside it.
func tsFiles(m *Module, out map[string][]byte) error {
partTypes := map[string]*typer{}
partFiles := map[string][]byte{}
for _, name := range sortedKeys(m.Parts) {
p := m.Parts[name]
d := seatdef.Definition{Grain: seatdef.Whole, Verbs: p.Verbs, Events: p.Events, Defs: p.Defs}
tp, err := declareAll(d, nil)
if err != nil {
return err
}
partTypes[name] = tp
var b bytes.Buffer
b.WriteString(tsHeader)
fmt.Fprintf(&b, "// The part %q several seats include whole: %s\n\n", p.Part, oneLine(p.Description))
tsNames(&b, "verbs", seatdef.VerbNames(p.Verbs))
tsNames(&b, "events", seatdef.MessageNames(p.Events))
tsTypes(&b, tp)
partFiles[name] = b.Bytes()
}
for _, name := range m.Order {
d := m.Seats[name]
if d.Grain != seatdef.Whole {
continue
}
dir := TSSeatDir(name)
tsPackage(out, dir, TSSeatPackage(m.TSPackage, name), fmt.Sprintf("The contract of the seat %s, generated from its definition.", name))
raw := m.Raw[name]
fromPart := map[string]string{}
for _, inc := range raw.Includes {
for _, t := range partTypes[inc].types {
fromPart[t.name] = inc
}
out[dir+"/src/parts/"+inc+".ts"] = partFiles[inc]
}
tp, err := declareAll(d, fromPart)
if err != nil {
return err
}
var b bytes.Buffer
b.WriteString(tsHeader)
fmt.Fprintf(&b, "// The contract of the seat %q, defined whole (seats/%s/seat.json): %s\n\n", d.Seat, d.Seat, oneLine(d.Description))
for _, inc := range raw.Includes {
fmt.Fprintf(&b, "import type * as %s from \"./parts/%s.js\";\n\n", tsName(inc), inc)
}
fmt.Fprintf(&b, "export const seat = %q;\n\n", d.Seat)
tsNames(&b, "verbs", seatdef.VerbNames(d.Verbs))
tsNames(&b, "accepts", seatdef.MessageNames(d.Accepts))
tsNames(&b, "events", seatdef.MessageNames(d.Events))
if f := d.Family; f != nil {
fmt.Fprintf(&b, "export const family = { name: %q, maxAgeDays: %d", f.Name, f.MaxAgeDays)
b.WriteString(" } as const;\n\n")
}
for _, e := range d.Events {
subj := seatdef.EventSubject(d.Seat, e.Name)
if e.ByCaller {
fmt.Fprintf(&b, "export function %sSubject(caller: string): string {\n return %q + caller;\n}\n\n", tsName(e.Name), subj+".")
} else {
fmt.Fprintf(&b, "export const %sSubject = %q;\n\n", tsName(e.Name), subj)
}
}
for _, a := range d.Accepts {
subj := seatdef.AcceptSubject(d.Seat, a.Name)
if a.ByCaller {
fmt.Fprintf(&b, "export function %sSubject(caller: string): string {\n return %q + caller;\n}\n\n", tsName(a.Name), subj+".")
} else {
fmt.Fprintf(&b, "export const %sSubject = %q;\n\n", tsName(a.Name), subj)
}
}
if len(d.HolderEvents) > 0 {
tsNames(&b, "holderEvents", holderNames(d.HolderEvents))
for _, e := range d.HolderEvents {
before, after, _ := strings.Cut(e.Subject, "<holder>")
fmt.Fprintf(&b, "export function %sHolderSubject(holder: string): string {\n return %q + holder + %q;\n}\n\n",
tsName(e.Name), before, after)
}
}
if len(d.States) > 0 {
if err := tsStates(&b, d.States); err != nil {
return err
}
}
for _, n := range sortedKeys(fromPart) {
fmt.Fprintf(&b, "export type %s = %s.%s;\n", n, tsName(fromPart[n]), n)
}
if len(fromPart) > 0 {
b.WriteString("\n")
}
tsTypes(&b, tp)
out[dir+"/src/index.ts"] = b.Bytes()
}
return nil
}
func holderNames(es []seatdef.HolderEvent) []string {
out := make([]string, len(es))
for i, e := range es {
out[i] = e.Name
}
return out
}
func tsNames(b *bytes.Buffer, what string, names []string) {
if len(names) == 0 {
return
}
fmt.Fprintf(b, "export const %s = {\n", what)
for _, n := range names {
fmt.Fprintf(b, " %s: %q,\n", tsName(n), n)
}
b.WriteString("} as const;\n\n")
}
func tsTypes(b *bytes.Buffer, tp *typer) {
for _, t := range tp.types {
if d := strings.TrimSpace(t.desc); d != "" {
fmt.Fprintf(b, "/** %s */\n", oneLine(d))
}
if t.t.kind != "struct" {
fmt.Fprintf(b, "export type %s = %s;\n\n", t.name, tsType(t.t))
continue
}
fmt.Fprintf(b, "export interface %s {\n", t.name)
for _, f := range t.t.fields {
if f.desc != "" {
fmt.Fprintf(b, " /** %s */\n", oneLine(f.desc))
}
opt := "?"
if f.required {
opt = ""
}
fmt.Fprintf(b, " %s%s: %s;\n", strconv.Quote(f.json), opt, tsType(f.t))
}
b.WriteString("}\n\n")
}
}
func tsType(t *typ) string {
if t.null {
n := *t
n.null = false
return tsType(&n) + " | null"
}
switch t.kind {
case "string":
if len(t.enum) > 0 {
parts := make([]string, len(t.enum))
for i, e := range t.enum {
parts[i] = strconv.Quote(e)
}
return strings.Join(parts, " | ")
}
return "string"
case "time":
return "string"
case "integer", "number":
return "number"
case "boolean":
return "boolean"
case "object":
return "Record<string, unknown>"
case "map":
return "Record<string, " + tsType(t.elem) + ">"
case "slice":
el := tsType(t.elem)
if strings.Contains(el, "|") {
el = "(" + el + ")"
}
return el + "[]"
case "ref":
return t.ref
}
return "unknown"
}