make site-headless builds the CLI without cgo (aego_nonative) and the static site with the wasm playground. deploy/ holds the image, the container nginx config (gzip_static with Vary), compose and deploy.sh run by the post-receive hook on mainserv. .gitignore ignores only the root site/ output; tools/gendocs/site was never committed because of it. Docs site: index and section summaries render code, bold and italic instead of raw markdown; search.json gets the plain text. Page summaries drop a leading list or quote marker. The nav toggle is three CSS bars centred in a 32px button. Playground: a loading overlay in the editor pane shows a spinner, a progress bar and the downloaded size while aego.wasm streams in, then 'Starting engine' until the runtime is ready; a failed start keeps the error on the overlay. docstool writes the wasm size and a version hash into index.html; play.js, play.css, wasm_exec.js and aego.wasm are referenced with ?v=<hash> so a deploy never mixes cached old files with new ones. Instruction budget: default 1M per callback (was 100k), playground 2M; the donut needs ~120k per frame. Playground: software 3D donut example, multi-line log entries render as monospace blocks, a fault is reported once instead of also as a "script stopped" log line. The compose project is named aego explicitly; the default name came from the deploy/ directory and collided with other stacks on the host.
271 lines
6.1 KiB
Go
271 lines
6.1 KiB
Go
package docstool
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"aego/engine/module"
|
|
"aego/project"
|
|
)
|
|
|
|
type Env struct {
|
|
Project *project.Project
|
|
Engine string
|
|
Modules []module.Module
|
|
Native func(args ...string) error
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
}
|
|
|
|
func (e *Env) export(path string) error {
|
|
if e.Native != nil {
|
|
args := []string{"docs", "export"}
|
|
if path != "" {
|
|
args = append(args, "--out", path)
|
|
}
|
|
return e.Native(args...)
|
|
}
|
|
reg, err := Registry(e.Project, e.Modules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return WriteExport(reg, Title(e.Project), path, e.Stdout)
|
|
}
|
|
|
|
func (e *Env) Export(path string) int {
|
|
if err := e.export(path); err != nil {
|
|
fmt.Fprintln(e.Stderr, "aego:", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (e *Env) Check(strict bool) int {
|
|
if e.Native != nil {
|
|
args := []string{"docs", "check"}
|
|
if strict {
|
|
args = append(args, "--strict")
|
|
}
|
|
if err := e.Native(args...); err != nil {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
reg, err := Registry(e.Project, e.Modules)
|
|
if err != nil {
|
|
fmt.Fprintln(e.Stderr, "aego:", err)
|
|
return 1
|
|
}
|
|
return Check(reg, strict, e.Stdout)
|
|
}
|
|
|
|
func (e *Env) goCmd(dir string, env []string, args ...string) error {
|
|
cmd := exec.Command("go", args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(os.Environ(), env...)
|
|
cmd.Stdout = e.Stdout
|
|
cmd.Stderr = e.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func (e *Env) Build(out string, play bool) error {
|
|
if e.Engine == "" {
|
|
return errors.New("cannot locate the aego source tree; pass --engine <dir> or set AEGO_ROOT")
|
|
}
|
|
abs, err := filepath.Abs(out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(abs, 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp, err := os.CreateTemp("", "aego-docs-*.json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp.Close()
|
|
defer os.Remove(tmp.Name())
|
|
if err := e.export(tmp.Name()); err != nil {
|
|
return err
|
|
}
|
|
args := []string{"run", ".", "-in", tmp.Name(), "-out", abs}
|
|
if play {
|
|
args = append(args, "-playground")
|
|
}
|
|
if err := e.goCmd(filepath.Join(e.Engine, "tools", "gendocs"), nil, args...); err != nil {
|
|
return fmt.Errorf("gendocs failed: %w", err)
|
|
}
|
|
if play {
|
|
return e.playground(filepath.Join(abs, "play"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(dst, data, 0o644)
|
|
}
|
|
|
|
func (e *Env) playground(dir string) error {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
wasm := filepath.Join(dir, "aego.wasm")
|
|
if err := e.goCmd(e.Engine, []string{"GOOS=js", "GOARCH=wasm"}, "build", "-trimpath", "-ldflags", "-s -w", "-o", wasm, "./cmd/aego-playground"); err != nil {
|
|
return fmt.Errorf("playground build failed: %w", err)
|
|
}
|
|
root, err := exec.Command("go", "env", "GOROOT").Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := copyFile(filepath.Join(strings.TrimSpace(string(root)), "lib", "wasm", "wasm_exec.js"), filepath.Join(dir, "wasm_exec.js")); err != nil {
|
|
return err
|
|
}
|
|
web := filepath.Join(e.Engine, "cmd", "aego-playground", "web")
|
|
files, err := os.ReadDir(web)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := os.Stat(wasm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blob, err := os.ReadFile(wasm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h := sha256.New()
|
|
h.Write(blob)
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(web, f.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.Write(data)
|
|
}
|
|
version := hex.EncodeToString(h.Sum(nil)[:5])
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
continue
|
|
}
|
|
src, dst := filepath.Join(web, f.Name()), filepath.Join(dir, f.Name())
|
|
if f.Name() != "index.html" {
|
|
if err := copyFile(src, dst); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data = bytes.ReplaceAll(data, []byte(`data-wasm-size="0"`), []byte(`data-wasm-size="`+strconv.FormatInt(info.Size(), 10)+`"`))
|
|
data = bytes.ReplaceAll(data, []byte(`data-version="dev"`), []byte(`data-version="`+version+`"`))
|
|
data = bytes.ReplaceAll(data, []byte(`?v=dev"`), []byte(`?v=`+version+`"`))
|
|
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Env) watched() []string {
|
|
var roots []string
|
|
if e.Engine != "" {
|
|
roots = append(roots, e.Engine)
|
|
}
|
|
if e.Project != nil && e.Project.Root != e.Engine {
|
|
roots = append(roots, e.Project.Root)
|
|
}
|
|
return roots
|
|
}
|
|
|
|
func skipDir(name string) bool {
|
|
return strings.HasPrefix(name, ".") || name == "site" || name == "node_modules" || name == "third_party"
|
|
}
|
|
|
|
func (e *Env) stamp() string {
|
|
var n, size int64
|
|
var latest time.Time
|
|
for _, root := range e.watched() {
|
|
filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
if p != root && skipDir(d.Name()) {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
slash := filepath.ToSlash(p)
|
|
doc := strings.HasSuffix(slash, ".md") && strings.Contains(slash, "/res/docs/")
|
|
if !doc && filepath.Ext(p) != ".aes" {
|
|
return nil
|
|
}
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
n++
|
|
size += info.Size()
|
|
if info.ModTime().After(latest) {
|
|
latest = info.ModTime()
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
return strconv.FormatInt(n, 10) + ":" + strconv.FormatInt(size, 10) + ":" + latest.String()
|
|
}
|
|
|
|
func (e *Env) watch(out string) {
|
|
last := e.stamp()
|
|
for range time.Tick(time.Second) {
|
|
s := e.stamp()
|
|
if s == last {
|
|
continue
|
|
}
|
|
last = s
|
|
start := time.Now()
|
|
if err := e.Build(out, false); err != nil {
|
|
fmt.Fprintln(e.Stderr, "docs:", err)
|
|
continue
|
|
}
|
|
fmt.Fprintf(e.Stdout, "docs: rebuilt in %s\n", time.Since(start).Round(time.Millisecond))
|
|
}
|
|
}
|
|
|
|
func noCache(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (e *Env) Serve(addr, out string, play bool) error {
|
|
if err := e.Build(out, play); err != nil {
|
|
return err
|
|
}
|
|
go e.watch(out)
|
|
fmt.Fprintf(e.Stdout, "docs: serving %s on http://%s\n", out, addr)
|
|
return http.ListenAndServe(addr, noCache(http.FileServer(http.Dir(out))))
|
|
}
|