Files
aego/app/docstool/site.go
T

308 lines
7.0 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"
"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
Repo string
}
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()
}
const defaultBranch = "master"
func (e *Env) git(args ...string) string {
out, err := exec.Command("git", append([]string{"-C", e.Engine}, args...)...).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func (e *Env) commit() string {
return e.git("rev-parse", "--short", "HEAD")
}
func (e *Env) branch() string {
if b := e.git("rev-parse", "--abbrev-ref", "HEAD"); b != "" && b != "HEAD" {
return b
}
return defaultBranch
}
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, "-version", engine.Version}
if play {
args = append(args, "-playground")
}
if e.Repo != "" {
args = append(args, "-repo", e.Repo, "-edit", strings.TrimRight(e.Repo, "/")+"/src/branch/"+e.branch())
}
if c := e.commit(); c != "" {
args = append(args, "-commit", c)
}
if hero := filepath.Join(e.Engine, "docs", "hero.png"); fileExists(hero) {
args = append(args, "-hero", hero)
}
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 fileExists(p string) bool {
st, err := os.Stat(p)
return err == nil && !st.IsDir()
}
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))))
}