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

@@ -1,14 +1,14 @@
package main
// Thin CLI wrapper around kjol/bundler. The bundler wires its own Go-native
// Solid JSX compiler (see bundler.Build), so this wrapper carries no build logic.
// Thin CLI wrapper around kjol/webbundler. The bundler wires its own Go-native
// Solid JSX compiler (see webbundler.Build), so this wrapper carries no build logic.
import (
"flag"
"fmt"
"os"
"kjol/bundler"
"kjol/webbundler"
)
func main() {
@@ -19,15 +19,15 @@ func main() {
genTS := flag.String("gen-ts", "", "Directory for the generated TS icon registry (default <app>/src/ui/generated).")
flag.Parse()
cfg := bundler.Config{
cfg := webbundler.Config{
AppFrontend: *app,
WebDir: *web,
Output: *out,
GenGoDir: *genGo,
GenTSDir: *genTS,
}
if err := bundler.Build(cfg); err != nil {
if err := webbundler.Build(cfg); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}

View File

@@ -33,10 +33,30 @@ a `static` route, and fetching only exists on the client, so the fetches no-op
during SSR: the server pre-renders the page's **spinner**, and the client runs
them for real after hydration.
Styling is **Tailwind**: the dev server (and `build.sh`) run `kjol/cmd/twcss`,
which scans the Go markup + the `webui` kit for utility classes and compiles
`css/app.css``wwwroot/app.css` with kjol's native Tailwind v4 engine. There is
**no Bootstrap and no hand-written CSS**. (`build.sh` does a one-off build.)
Styling is **Tailwind**: the build runs `kjol/cmd/twcss`, which scans the Go markup +
the `webui` kit for utility classes and compiles `css/app.css``wwwroot/app.css` with
kjol's native Tailwind v4 engine (`kjol/tw`). There is **no Bootstrap and no
hand-written CSS**.
Saving a `.css` file recompiles **only** Tailwind and swaps the stylesheet into the live
page — no wasm rebuild, no reload, no lost state. Saving a `.go` file does the full
rebuild and hot-swaps the wasm.
## Building
The build is Go, not a shell script — `buildsteps/` holds the four steps (codegen →
Tailwind → wasm → `wasm_exec.js` shim), and both the one-off build and the dev server's
watch loop call the *same* functions, so they cannot drift apart.
```
go run ./build # one-off: codegen + Tailwind + wasm + shim
go run ./server # dev server: does the same build, then watches and hot-reloads
```
In VS Code these are the `gowasm: build` and `gowasm: dev server (hot reload)` tasks;
both run through `gowasm: prebuild` (codegen + Tailwind), which is also the
`preLaunchTask` of the debug configs — under the debugger the binary is built by Delve,
so nothing else would generate `app/*.gen.go`.
## This is a separate module

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env bash
# Pre-compile step: directive codegen, Tailwind CSS, WebAssembly build, JS shim.
# Run the dev server instead (go run ./server) for hot reload; this is for a
# one-off/production-style build. Run from anywhere.
set -euo pipefail
cd "$(dirname "$0")"
KJOL_GO="$(cd ../../.. && pwd)"
echo "==> Generating directive glue (//gowasm:page, //gowasm:layout, //gowasm:server)"
go run kjol/cmd/wasmgen ./app
echo "==> Compiling Tailwind CSS -> wwwroot/app.css (scanning webui + Go markup)"
# Run twcss from the kjol module root so the Tailwind engine's deps resolve there
# (not in this example module — keeps its go.mod lean).
( cd "$KJOL_GO" && 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' )
echo "==> Compiling ./wasm to wwwroot/app.wasm (GOOS=js GOARCH=wasm)"
GOOS=js GOARCH=wasm go build -o wwwroot/app.wasm ./wasm
echo "==> Copying Go's wasm_exec.js shim into wwwroot/"
GOROOT="$(go env GOROOT)"
shim="$GOROOT/lib/wasm/wasm_exec.js" # Go >= 1.24
[ -f "$shim" ] || shim="$GOROOT/misc/wasm/wasm_exec.js" # Go <= 1.23
rm -f wwwroot/wasm_exec.js # GOROOT copy is read-only; remove before overwriting
cp "$shim" wwwroot/wasm_exec.js
chmod u+w wwwroot/wasm_exec.js
echo "==> Done. Run the server with: go run ./server"
echo " then open http://localhost:8085"

View File

@@ -0,0 +1,41 @@
// Command build runs the example's full pre-compile step once: directive codegen,
// Tailwind, the wasm binary, and Go's JS shim.
//
// go run ./build # from cmd/examples/go-wasm-web
//
// For day-to-day work run the dev server instead (`go run ./server`) — it performs
// these same steps on every save and hot-swaps the result into the browser. This
// command is for a cold build, CI, or an editor's pre-launch task.
package main
import (
"log"
"os"
"gowasmweb/buildsteps"
)
func main() {
log.SetFlags(0)
steps := []struct {
name string
run func() ([]byte, error)
}{
{"generating directive glue (//gowasm:page, //gowasm:layout, //gowasm:server)", buildsteps.Codegen},
{"compiling Tailwind CSS -> wwwroot/app.css", buildsteps.Tailwind},
{"compiling ./wasm -> wwwroot/app.wasm (GOOS=js GOARCH=wasm)", buildsteps.Wasm},
{"copying Go's wasm_exec.js shim into wwwroot/", buildsteps.Shim},
}
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. Run the server with: go run ./server")
log.Println(" then open http://localhost:8085")
}

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

View File

@@ -13,15 +13,13 @@ import (
"flag"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"kjol/httputil"
"kjol/vdom"
"kjol/wasmdevserver"
"gowasmweb/app"
"gowasmweb/buildsteps"
)
func main() {
@@ -34,7 +32,8 @@ func main() {
Dir: "./wwwroot",
Watch: *watch,
WatchDirs: []string{"app", "wasm", "css", "../../../webui", "../../../vdom", "../../../wasmruntime", "../../../rsc"}, // example + kjol engine + kit
Build: buildWasm,
Build: buildsteps.All,
BuildCSS: buildsteps.Tailwind, // a .css save skips codegen+wasm and hot-swaps the stylesheet
Render: render,
Document: document,
Handle: apiRoutes,
@@ -89,29 +88,3 @@ func document(inner string) string {
</body>
</html>`
}
// buildWasm runs the directive codegen (kjol/cmd/wasmgen), compiles the Tailwind
// CSS (kjol/cmd/twcss, scanning the Go markup + webui kit), then compiles ./wasm
// to wwwroot/app.wasm. Returned combined output is shown in the browser overlay
// on failure.
func buildWasm() ([]byte, error) {
if out, err := exec.Command("go", "run", "kjol/cmd/wasmgen", "./app").CombinedOutput(); err != nil {
return out, err
}
// Compile Tailwind from the kjol module root (so the engine's deps resolve),
// scanning the webui kit + this example's Go markup for utility candidates.
tw := 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")
tw.Dir = "../../.." // kjol/go
if out, err := tw.CombinedOutput(); err != nil {
return out, err
}
cmd := exec.Command("go", "build", "-o", filepath.Join("wwwroot", "app.wasm"), "./wasm")
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
return cmd.CombinedOutput()
}

View File

@@ -2,8 +2,8 @@
// The wasm client entry point (main.go) builds only under GOOS=js GOARCH=wasm.
// This native placeholder keeps the package buildable on the host so a plain
// `go build ./...` succeeds; the real client is built by build.sh / the dev
// server with GOOS=js GOARCH=wasm.
// `go build ./...` succeeds; the real client is built by ./build (or the dev
// server) with GOOS=js GOARCH=wasm.
package main
func main() {}

File diff suppressed because one or more lines are too long

View File

@@ -14,7 +14,7 @@ import (
"fmt"
"os"
"kjol/bundler"
"kjol/tw"
)
func main() {
@@ -32,7 +32,7 @@ func main() {
fmt.Fprintln(os.Stderr, "twcss:", err)
os.Exit(1)
}
css, err := bundler.CompileTailwind(string(src), *base, flag.Args())
css, err := tw.CompileFiles(string(src), *base, flag.Args())
if err != nil {
fmt.Fprintln(os.Stderr, "twcss:", err)
os.Exit(1)