rename packages, refactor out tailwind compiler,

This commit is contained in:
2026-07-13 15:06:09 -04:00
parent c5b14d7c41
commit d52151cc1a
62 changed files with 1446 additions and 460 deletions

View File

@@ -0,0 +1,106 @@
// Package buildsteps is the example's build pipeline: directive codegen, Tailwind,
// the wasm binary, and Go's JS shim.
//
// It is Go, not a shell script, for three reasons. The dev server needs to call these
// steps on every save and cannot shell out to bash on Windows. Editors need to run
// them as tasks, and a task that only works on one platform is a task half the team
// cannot use. And the one-off build and the watch build must be the SAME steps — the
// moment they are two scripts, they drift, and the bug only shows up in whichever one
// you use less.
//
// Run the whole thing with `go run ./build`.
package buildsteps
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
// kjolRoot is the kjol Go module root, relative to the example directory. The Tailwind
// and codegen commands are run FROM there so the engine's dependencies resolve in
// kjol's own go.mod, and this example's stays lean.
const kjolRoot = "../../.."
// Wwwroot is where every build artefact lands, and what the dev server serves.
const Wwwroot = "wwwroot"
// Codegen regenerates app/*.gen.go from the //gowasm: directives — the routes, the
// layouts, and the client stubs for server components. It runs FIRST: everything after
// it compiles the code it writes.
func Codegen() ([]byte, error) {
return run("go", "run", "kjol/cmd/wasmgen", "./app")
}
// Tailwind compiles css/app.css to wwwroot/app.css, scanning the webui kit and this
// example's Go markup for utility candidates.
//
// It scans .go files, which is the whole point of kjol's native engine: the markup is
// written in Go, so that is where the class names are. There is no JS build here at
// all.
func Tailwind() ([]byte, error) {
cmd := exec.Command("go", "run", "./cmd/twcss",
"-entry", "cmd/examples/go-wasm-web/css/app.css",
"-out", "cmd/examples/go-wasm-web/wwwroot/app.css",
"-base", ".",
"webui/**/*.go",
"cmd/examples/go-wasm-web/app/**/*.go",
"cmd/examples/go-wasm-web/server/**/*.go",
)
cmd.Dir = kjolRoot
return cmd.CombinedOutput()
}
// Wasm compiles ./wasm to wwwroot/app.wasm.
func Wasm() ([]byte, error) {
cmd := exec.Command("go", "build", "-o", filepath.Join(Wwwroot, "app.wasm"), "./wasm")
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
return cmd.CombinedOutput()
}
// Shim copies Go's wasm_exec.js into wwwroot. It is the loader the browser needs to
// start a Go wasm binary, it ships with the toolchain, and it must match the compiler
// that produced the binary — so it is copied from GOROOT rather than vendored.
func Shim() ([]byte, error) {
out, err := exec.Command("go", "env", "GOROOT").Output()
if err != nil {
return out, err
}
goroot := strings.TrimSpace(string(out))
for _, src := range []string{
filepath.Join(goroot, "lib", "wasm", "wasm_exec.js"), // Go >= 1.24
filepath.Join(goroot, "misc", "wasm", "wasm_exec.js"), // Go <= 1.23
} {
b, err := os.ReadFile(src)
if err != nil {
continue
}
dst := filepath.Join(Wwwroot, "wasm_exec.js")
// The GOROOT copy is read-only, and so is the copy we made last time. Remove it
// first, or the write fails with a permission error that says nothing useful.
os.Remove(dst)
return nil, os.WriteFile(dst, b, 0o644)
}
return nil, os.ErrNotExist
}
// All is the full build, in order. It is what the dev server runs on a code change and
// what `go run ./build` runs once.
//
// Returned output is the failing command's combined stdout+stderr, which the dev server
// puts straight into the browser's error overlay — so a compile error lands in front of
// you rather than in a terminal you were not looking at.
func All() ([]byte, error) {
for _, step := range []func() ([]byte, error){Codegen, Tailwind, Wasm, Shim} {
if out, err := step(); err != nil {
return out, err
}
}
return nil, nil
}
func run(name string, args ...string) ([]byte, error) {
return exec.Command(name, args...).CombinedOutput()
}