Skip to content

Generating wrappers

The generator reads the define() call itself. There is no second file to keep in step, and no manifest to maintain until you want one.

Terminal window
alacris-go generate ./web/components -o ./internal/components

For each component: a tag constant, a props struct, a constructor, slot and event constants, typed event details, and the theming contract.

internal/components/components_gen.go
const UserCardTag = "user-card"
const (
UserCardSlotTitle = "title"
)
const (
UserCardEventGreet = "greet"
)
type UserCardGreetDetail struct {
Name string `json:"name"`
}
var UserCardVars = alacris.Vars(
"--card-bg", // the background (defaults to #fff)
)
type UserCardProps struct {
Name string
Age int
Tags []string
}
func UserCard(p UserCardProps) *alacris.Element

Plus a shared file with Tags, the list every component in the package registers, which is what alacris.Pending wants:

@alacris.Pending{Tags: components.Tags}
What a call expands to
// What ui.TodoList(...) expands to. The generator knows every prop's
// default, so Filter: "all" would have been left off the element.
return alacris.E("ala-todo-list").
ID("todos").
Prop("items", []todoItem{{ID: 1, Text: "Read the docs"}}).
Prop("filter", "active").
On("add", "add-todo").
On("toggle", "toggle-todo")
renders verified by go test
<ala-todo-list
items="[{&#34;id&#34;:1,&#34;text&#34;:&#34;Read the docs&#34;,&#34;done&#34;:false}]"
filter="active"
id="todos"
data-ala-on="add:add-todo toggle:toggle-todo"
></ala-todo-list>
which the browser turns into the real runtime, upgrading that markup

What a generated wrapper expands to.

define() says a prop’s name and, through its default, how it is coerced. It does not say that tags holds strings, that a greet event exists, or which slots a component renders. JSDoc above the call does.

/**
* A person, at a glance.
*
* @prop {string[]} tags the labels shown under the name
* @prop {integer} age whole years
* @fires greet {name: string} - the user said hello
* @slot title - replaces the heading
* @cssprop [--card-bg=#fff] - the background
* @goname PersonCard
*/
define('user-card', {
props: { name: 'anon', age: 0, tags: [] },
setup,
});

Without @prop {string[]} tags, an empty array default gives []any, because an empty array says nothing about its elements. The tag is how you say otherwise. Every tag is listed in the JSDoc reference.

The scanner is a scanner, not a JavaScript engine. A props object it cannot read as literal data is an error, never a guess:

define('x-y', { props: buildProps(), setup });
// t.js:1: <x-y>: props is expression, not an object literal;
// declare the props in a manifest instead
define('x-y', { props: { ...base, a: 1 }, setup });
// t.js:1: <x-y>: props holds a spread or a computed key, so the full set of
// props is not visible here; declare this component in a manifest instead

Generating the props it can see would produce a struct quietly missing fields, which is worse than stopping.

When a component builds its props at runtime, describe it by hand:

  1. Write out what the scanner found:

    Terminal window
    alacris-go manifest ./web -o alacris.components.json
  2. Edit it. Add the component the scanner refused, fix a type, name a Go type for an event detail.

  3. Generate from it, alone or mixed with sources:

    Terminal window
    alacris-go generate ./alacris.components.json ./web/other -o ./internal/components

The manifest is the same shape the scanner produces, so nothing is lost by switching one component to it.

A tag becomes a Go identifier: user-cardUserCard. Recognised initialisms keep their casing, so avatarUrlAvatarURL and itemIdItemID.

A project prefix on every tag would put it on every identifier too, so strip it:

Terminal window
alacris-go generate ./web -o ./internal/components -strip ala-
# <ala-counter> generates Counter, not AlaCounter

Collisions are refused, including ones stripping introduces:

alacris-go: with -strip "ala-", <ala-todo-item> and <todo-item> both generate TodoItem

@goname settles it.

Do not generate your own wrappers into a package also named ui if you import github.com/bmartel/alacris-go/ui. That is the design system. Put app wrappers in ./internal/components (or similar), or alias one of the imports.

main.go
//go:generate go run github.com/bmartel/alacris-go/cmd/alacris-go generate ./web -o ./internal/components -strip ala-
//go:generate go run github.com/a-h/templ/cmd/templ@latest generate

The wrappers first. The templates use them.

In CI, check instead:

- name: Generated wrappers are up to date
run: go run ./cmd/alacris-go check ./web -o ./internal/components -strip ala-

check compares byte for byte and reports what differs, including files left behind by a component that was renamed or deleted. Output does not depend on the directory you run from, so the same command works locally and in CI.

  • Directoryweb/
    • components.js your define() calls
  • Directoryinternal/components/ generated app wrappers; do not edit
    • components_gen.go
    • alacris_gen.go Tags, package docs
  • alacris.components.json optional, only for what the scanner cannot read

Generated files carry a Code generated by alacris-go … DO NOT EDIT. header and are marked linguist-generated by the repository’s .gitattributes, so review diffs stay readable.