Files
summercms/cmd/summer/docs.go
Jakub Zych 8d4d7ce7a7 feat(bonfire): global flags, Artisan list and exit codes
- Global options -v/-vv/-vvv, -q, --ansi/--no-ansi, -n and --env on every
  command; PeekGlobals reads them before config loads, OnGlobals hooks
  re-apply them after parsing (diary.ApplyGlobals in the generated main)
- Bare binary, --help and list print the Artisan list with the version
  header from BuildVersion and namespace blocks
- Execute runs the root through fang (no version, man page or completions)
  and returns 0, 1 or 2; Fail renders errors before the root exists
- NewRoot rejects duplicate command names (ErrDuplicateCommand) and flags
  that reuse a global option (ErrReservedFlag)
- Generated main and summer use PeekGlobals, compass.Open with --env,
  diary.FromGlobals and bonfire.Execute; docs checker knows the globals
2026-10-21 12:13:23 +02:00

177 lines
6.6 KiB
Go

package main
import (
"context"
"errors"
"os"
"os/signal"
"sync"
"syscall"
"git.golem15.com/golem15/summercms/internal/docsite"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/conga"
"git.golem15.com/golem15/summercms/modules/flare"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo"
"git.golem15.com/golem15/summercms/modules/surf"
)
func docsBuildCommand() bonfire.Command {
return bonfire.Command{
Name: "docs:build",
Description: "Build the documentation site",
Flags: []bonfire.Flag{
{Name: "root", Description: "Repository root; src= paths and modules/ resolve against it", Default: "."},
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
{Name: "out", Description: "Output directory (default <root>/site)"},
{Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"},
{Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"},
{Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"},
{Name: "check", Description: "Validate the docs and write nothing", Bare: true},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
opts := docsOptions(in)
if flagTrue(in, "check") {
problems, err := docsite.Check(opts)
if err != nil {
return err
}
if len(problems) > 0 {
return reportDocsProblems(out, "docs:build", problems)
}
out.Printf("docs:build: no problems found\n")
return nil
}
result, problems, err := docsite.Build(opts)
if err != nil {
return err
}
if len(problems) > 0 {
return reportDocsProblems(out, "docs:build", problems)
}
out.Printf("docs:build: wrote %d pages to %s\n", result.Pages, result.Out)
return nil
},
}
}
func docsSyncCommand() bonfire.Command {
return bonfire.Command{
Name: "docs:sync",
Description: "Rewrite src= code blocks from their sources",
Flags: []bonfire.Flag{
{Name: "root", Description: "Repository root; src= paths resolve against it", Default: "."},
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
result, problems, err := docsite.Sync(docsOptions(in))
if err != nil {
return err
}
if len(problems) > 0 {
return reportDocsProblems(out, "docs:sync", problems)
}
if result.Snippets == 0 {
out.Printf("docs:sync: all snippets up to date\n")
return nil
}
out.Printf("docs:sync: updated %d snippets in %d files\n", result.Snippets, result.Files)
return nil
},
}
}
func docsServeCommand() bonfire.Command {
return bonfire.Command{
Name: "docs:serve",
Description: "Build the documentation site and preview it on a local address",
Flags: []bonfire.Flag{
{Name: "root", Description: "Repository root; src= paths and modules/ resolve against it", Default: "."},
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
{Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"},
{Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"},
{Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"},
{Name: "addr", Description: "Listen address; must be loopback unless --allow-remote", Default: docsite.DefaultServeAddr},
{Name: "allow-remote", Description: "Allow a non-loopback --addr (serves the docs on the network)", Bare: true},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
addr, _ := in.Flag("addr")
if addr == "" {
addr = docsite.DefaultServeAddr
}
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
return docsite.Serve(ctx, docsOptions(in), addr, flagTrue(in, "allow-remote"), outputWriter{mu: &sync.Mutex{}, out: out})
},
}
}
// outputWriter adapts bonfire.Output to io.Writer for docsite.Serve, which
// writes from its watch goroutine too.
type outputWriter struct {
mu *sync.Mutex
out bonfire.Output
}
func (w outputWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
w.out.Printf("%s", p)
return len(p), nil
}
func docsOptions(in bonfire.Input) docsite.Options {
opts := docsite.Options{Commands: docsCommands()}
opts.Root, _ = in.Flag("root")
opts.Src, _ = in.Flag("src")
opts.Out, _ = in.Flag("out")
opts.BaseURL, _ = in.Flag("base-url")
opts.SiteURL, _ = in.Flag("site-url")
opts.SiteLabel, _ = in.Flag("site-label")
return opts
}
// docsCommands collects the command names docs pages may show: the summer
// tool's own commands, list and help (added by bonfire.NewRoot), and every
// command the generated application main
// registers (TestDocsCommandsMirrorGeneratedMain keeps this list in step
// with internal/build) plus the realtime and push commands applications
// append. The constructors only capture the app, so an empty config is
// enough; nothing runs.
func docsCommands() *docsite.Commands {
// list and help come from bonfire.NewRoot, in both programs.
tool := []string{"list", "help"}
for _, c := range toolCommands() {
tool = append(tool, c.Name)
}
app := backpack.New(&compass.Config{})
var appCmds []bonfire.Command
appCmds = append(appCmds, lagoon.RuntimeCommands(app, nil)...)
appCmds = append(appCmds, lagoon.KeyGenerateCommand())
appCmds = append(appCmds, conga.RuntimeCommands(app, nil)...)
appCmds = append(appCmds, surf.ServeCommand(app, nil))
appCmds = append(appCmds, surf.RouteListCommand(app, nil))
appCmds = append(appCmds, cabana.RuntimeCommands(app)...)
appCmds = append(appCmds, centrifugo.Commands(app)...)
appCmds = append(appCmds, flare.Commands(app)...)
names := []string{"list", "help"}
for _, c := range appCmds {
names = append(names, c.Name)
}
return &docsite.Commands{Tool: tool, App: names}
}
// reportDocsProblems prints one line per problem and a summary, and returns
// a short error so the binary exits 1.
func reportDocsProblems(out bonfire.Output, cmd string, problems []docsite.Problem) error {
for _, p := range problems {
out.Printf("%s\n", p)
}
out.Printf("%s: %d problems, nothing written\n", cmd, len(problems))
return errors.New(cmd + " failed")
}