Update kjol website with C documentation
This commit is contained in:
177
go/cmd/kjol-web/build/build.go
Normal file
177
go/cmd/kjol-web/build/build.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Package build is the build pipeline: directive codegen, Tailwind, the wasm binary,
|
||||
// the Solid bundle, and Go's JS shim.
|
||||
//
|
||||
// It used to be two packages — a `buildsteps` library and a `build` command that did
|
||||
// nothing but loop over it and print. There was nothing for that split to be: the
|
||||
// library had exactly two importers, both in this directory tree, and a package with no
|
||||
// outside consumers is an import statement pretending to be a boundary. It is one
|
||||
// package now.
|
||||
//
|
||||
// It cannot be `package main`, because the dev server imports it and Go will not let you
|
||||
// import a main. So the cold build is a flag on the server rather than a second binary:
|
||||
//
|
||||
// go run ./server -build # build once and exit
|
||||
// go run ./server # build, then watch and serve
|
||||
//
|
||||
// It is Go rather than a shell script for three reasons. The dev server has 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 cold 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.
|
||||
package build
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"kjol/jsbundler"
|
||||
)
|
||||
|
||||
// kjolRoot is the kjol Go module root, relative to the app. The Tailwind and codegen
|
||||
// commands are run FROM there so the engine's dependencies resolve in kjol's own go.mod,
|
||||
// and this app's stays lean.
|
||||
const kjolRoot = "../.."
|
||||
|
||||
// Wwwroot is where every build artefact lands, and what the server serves.
|
||||
const Wwwroot = "wwwroot"
|
||||
|
||||
// Step is one named stage. Naming them lets the cold build narrate itself without the
|
||||
// watch loop having to care what they are called.
|
||||
type Step struct {
|
||||
Name string
|
||||
Run func() ([]byte, error)
|
||||
}
|
||||
|
||||
func Steps() []Step {
|
||||
return []Step{
|
||||
{"generating directive glue (//gowasm:page, //gowasm:layout, //gowasm:server)", Codegen},
|
||||
{"compiling Tailwind CSS -> wwwroot/app.css", Tailwind},
|
||||
{"compiling ./wasm -> wwwroot/app.wasm (GOOS=js GOARCH=wasm)", Wasm},
|
||||
{"bundling the Solid app -> wwwroot/bundle.min.{js,css} (TSX -> Solid -> esbuild)", JS},
|
||||
{"copying Go's wasm_exec.js shim into wwwroot/", Shim},
|
||||
}
|
||||
}
|
||||
|
||||
// All is the full build, in order. It is what the dev server runs on a code change.
|
||||
//
|
||||
// The returned bytes are 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 _, s := range Steps() {
|
||||
if out, err := s.Run(); err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Cold runs every step once, narrating as it goes, and exits non-zero on the first
|
||||
// failure. This is `go run ./server -build`: CI, a cold start, or an editor's pre-launch
|
||||
// task — anywhere there is nobody watching a browser overlay.
|
||||
func Cold() {
|
||||
log.SetFlags(0)
|
||||
for _, s := range Steps() {
|
||||
log.Println("==>", s.Name)
|
||||
if out, err := s.Run(); err != nil {
|
||||
os.Stderr.Write(out)
|
||||
log.Fatalln("build failed:", err)
|
||||
}
|
||||
}
|
||||
log.Println("==> Done. Serve it with: go run ./server")
|
||||
}
|
||||
|
||||
// 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 exec.Command("go", "run", "kjol/cmd/wasmgen", "./app").CombinedOutput()
|
||||
}
|
||||
|
||||
// Tailwind compiles css/app.css to wwwroot/app.css, scanning the webui kit, the lexer's
|
||||
// palette and this app'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. Nothing in this stage involves
|
||||
// JavaScript.
|
||||
//
|
||||
// EVERY package whose class names have to exist has to be listed here. That is not a
|
||||
// warning about carelessness — it is the failure mode: a package left off this list still
|
||||
// compiles, still renders, and just comes out unstyled, because the class it asked for was
|
||||
// never generated. kjol/lexer is here for exactly that reason; its whole output is class
|
||||
// names, and nothing else in the tree mentions text-teal-300.
|
||||
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",
|
||||
"lexer/**/*.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 Solid app: the SPA under /js, the server-rendered 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 browser's error 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. They never collide: different filenames, one static dir, one server.
|
||||
//
|
||||
// WebDir points at the shared tree — the kit, the vendored Solid runtime, the icon SVGs
|
||||
// and the @theme scaffold all live there.
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user