// 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() }