134 lines
4.8 KiB
Go
134 lines
4.8 KiB
Go
// 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"
|
|
|
|
"kjol/jsbundler"
|
|
)
|
|
|
|
// 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/kjol-web/css/app.css",
|
|
"-out", "cmd/kjol-web/wwwroot/app.css",
|
|
"-base", ".",
|
|
"webui/**/*.go",
|
|
"cmd/kjol-web/app/**/*.go",
|
|
"cmd/kjol-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()
|
|
}
|
|
|
|
// JS builds the OTHER half of the site: the Solid SPA under /js, the SSR'd public
|
|
// pages, and their stylesheet. It is kjol/jsbundler — TSX compiled to Solid by a Go
|
|
// program, bundled by esbuild's Go API, styled by kjol/tw — run in-process rather than
|
|
// shelled out to, so a compile error comes back as a Go error and lands in the dev
|
|
// server's browser overlay like every other failure.
|
|
//
|
|
// It writes bundle.min.{js,css} and public.bundle.min.{js,css} into the SAME wwwroot as
|
|
// the wasm build. The two halves never collide: different filenames, one static dir, one
|
|
// server.
|
|
//
|
|
// -web points at the shared tree, which is where the kit, the vendored Solid runtime,
|
|
// the icon SVGs and the @theme scaffold all live.
|
|
func JS() ([]byte, error) {
|
|
err := jsbundler.Build(jsbundler.Config{
|
|
AppFrontend: "frontend",
|
|
WebDir: filepath.Join(kjolRoot, "jsruntime"),
|
|
Output: Wwwroot,
|
|
GenTSDir: filepath.Join("frontend", "src", "ui", "generated"),
|
|
})
|
|
if err != nil {
|
|
return []byte(err.Error()), err
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// 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, JS, 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()
|
|
}
|